TAMPER
SIGNAL
← Docs home

Reference

Mounting the signal

The light is most useful where the numbers live: on the dashboard itself. Everything here re-verifies the chain in the viewer's own browser using Web Crypto, with no build step, no framework requirement, and no server state. The UI is two things, always shipped together: the light — a small pill in the header, the lightest touch — and the room — the one robust surface behind it, where the light's "view receipts" link lands.

Serve the page over HTTP

Every surface fetch()es the chain (and table.json), which the browser blocks on a file:// page. Opening index.html directly leaves the signal silently unverified. Serve the page over HTTP: any static server works, and tamper-signal serve is the one-liner for local dev. For a truly offline recipient, send a verified bundle (tamper-signal export --bundle) and verify with the CLI instead.

One light, one room

SurfaceImportFor
The lighttamper-signal/lightA small pill in the dashboard header. The whole footprint on your page.
The roomtamper-signal/roomEverything behind the light: the attested table, the chain, the break exhibit, the inspector, the log, the custody timeline, the export.

Compat shims (2.x): tamper-signal/table and tamper-signal/console keep their old signatures and now render the room's table and console presets. tamper-signal/badge's renderReceiptBadge is deprecated (removed in 3.0).

The inline status light

A dark, monospace pill that shows the traffic light and expands to a popover: a per-stage table when green, the caveat list when yellow, the broken link and totals delta when red.

<!-- serve receipts/ statically, then mount in the header -->
<script type="module">
  import { mountTamperSignal } from "/badge/light.js";
  mountTamperSignal(document.querySelector("header"), "/receipts/chain.json");
</script>

With a bundler it is the same call from the package:

import { mountTamperSignal } from "tamper-signal/light";

Options

OptionEffect
watchRe-verify every N milliseconds and pulse the pill on a transition.
warnDriftTreat control-total movement as a yellow caveat, matching --warn-drift.
receiptsHrefWhere the popover's "view receipts" link points.
surface: "dark"Invert the pill for a dark host. Default is "light". (invert: true is a shortcut.)
Leave the pill alone

The pill is intentionally dark and monospace so it reads as an instrument, not a host control. Place it at the right end of your header, after your own controls, and do not restyle it to match the page. On a dark host, pass { surface: "dark" } so it inverts cleanly.

React and web component

// React
import { TamperSignal } from "tamper-signal/react";
<TamperSignal chain="/receipts/chain.json" />

// Plain HTML, via tamper-signal/element
<tamper-signal chain="/receipts/chain.json"></tamper-signal>

The room

The one surface behind the light. The attested table is the landing plane — re-hashed in the viewer's browser against the final receipt, so its VERIFIED means the rows on screen are byte for byte the attested data — with the chain as a provenance rail above it, the break exhibit in business numbers when the chain is red, and the receipt inspector, CLI-mirror event log, and chain-of-custody timeline one drawer away. Green earns silence; the layout leads with whatever the verdict demands.

# 1. export the canonical table as the last pipeline step (Python or Node)
tamper-signal export receipts/chain.json --data dashboard.xlsx

# 2. mount it
<script type="module">
  import { mountSignalRoom } from "/badge/room.js";
  mountSignalRoom(document.querySelector("#data-tab"), "/receipts/chain.json");
</script>

Or the web component (works from React JSX too — importing tamper-signal/room registers it and ships the JSX typing):

<script type="module" src="/badge/room.js"></script>
<tamper-signal-room chain="/receipts/chain.json"></tamper-signal-room>

Attributes: chain (required), table, timeline, pub-key, watch, warn-drift, strict, max-rows, focus, preset (room | table | console), density (embedded | page). After each verification the room fires a bubbling tamper-signal:state event carrying { state, attested, strict }; the documented host gate is strict && (state === "red" || !attested). Skipping the export step degrades honestly: no table.json shows a grey "no attested table published" slab, a stale one reads NOT THE ATTESTED DATA with the re-run command.

Every attach helper serves the room automatically at /tamper-signal/receipts and points the light's "view receipts" link there with ?focus=auto, so a red chain opens scrolled to the break. /tamper-signal/console stays reachable, serving the room with its rail open.

One-call framework helpers

Each attach helper serves three things from one call — your receipts/ directory, the verifier assets, and the room page — and returns a ready-to-drop snippet with the light's receiptsHref pre-wired to the room, so you do not wire paths by hand. You structurally cannot ship the light without a live room behind it. (room=False / { room: false } opts out; not recommended — the light will link to raw JSON.)

# Flask
from tamper_signal.flask_ext import attach
signal = attach(app, receipts_dir="receipts/")   # {{ signal.snippet | safe }}

# FastAPI
from tamper_signal.fastapi_ext import attach
signal = attach(app, receipts_dir="receipts/")
// Express
import { tamperSignal } from "tamper-signal/express";
const signal = tamperSignal(app, { receiptsDir: "receipts/" });
// render signal.snippet once in your layout
HostHow
Flasktamper_signal.flask_ext.attach(app, ...)
FastAPItamper_signal.fastapi_ext.attach(app, ...)
ExpresstamperSignal(app, { receiptsDir }) from tamper-signal/express
Next.jsCopy receipts/ into public/receipts/ during the pipeline run, then <TamperSignal chain="/receipts/chain.json" />
Streamlitfrom tamper_signal.streamlit_ext import signal, verified_dataframe (server-side verified, a weaker check)

Flag a broken metric in place

The signal can reach into the page and outline the exact numbers that no longer descend from the source. Add data-receipt-column to any element that displays a metric, naming the column it comes from. When the chain breaks at a link where that column moved, the signal outlines the element and tags it tamper signal: unverified value.

<div class="kpi" data-receipt-column="spend_(usd)">
  $98,441.02
</div>

Column names are the normalized keys from the receipts' control totals: lowercased, spaces turned to underscores. Read them from numeric_sums and null_counts in the receipt files.

A real limit worth knowing

Only plain decimals are summed into numeric_sums. A thousands-grouped string like "289,084" or "1 198 372" is not coerced to a number, so that column never reaches the totals, and data-receipt-column on it cannot flag a change. The fix is upstream: a signed normalize stage that turns the column into plain decimals. The ingest command prints a warning naming such columns, and groupedNumericColumns(records) finds them in code.

Verify your wiring