tamper-signal 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +130 -90
- package/README.md +27 -18
- package/badge/badge.js +147 -5
- package/badge/console.js +47 -459
- package/badge/room.d.ts +118 -0
- package/badge/room.js +1629 -0
- package/badge/table.js +69 -524
- package/node/cli.js +38 -4
- package/node/express.d.ts +67 -5
- package/node/express.js +98 -22
- package/node/test/append_period.test.js +1 -1
- package/node/test/badge.test.js +7 -2
- package/node/test/cli_color.test.js +9 -1
- package/node/test/domstub.js +214 -0
- package/node/test/express.test.js +45 -6
- package/node/test/pipeline.test.js +20 -1
- package/node/test/room_state.test.js +200 -0
- package/node/test/stale_table.test.js +69 -0
- package/node/test/vendored_skew.test.js +77 -0
- package/node/test/verify_memo.test.js +133 -0
- package/node/test/vocab.test.js +60 -0
- package/node/test/zip.test.js +4 -4
- package/node/wrapper.js +13 -1
- package/package.json +11 -3
package/AGENTS.md
CHANGED
|
@@ -569,7 +569,15 @@ mode 0400, never in the process environment) — do **not** put the key material
|
|
|
569
569
|
in `EnvironmentFile`, which would expose it via `/proc/<pid>/environ`. Keep the
|
|
570
570
|
key file `0600`; the watcher fails closed if it is group/world-readable.
|
|
571
571
|
|
|
572
|
-
## 6. Add the
|
|
572
|
+
## 6. Add the light to the host UI (the room is already served)
|
|
573
|
+
|
|
574
|
+
The UI is two things, always shipped together: **the light** (the inline
|
|
575
|
+
status pill, the lightest touch) and **the room** (the one robust surface
|
|
576
|
+
behind it — the attested data table with the chain rail, break exhibit,
|
|
577
|
+
receipt inspector, event log, chain of custody, and evidence export). The
|
|
578
|
+
attach helpers below serve the room automatically and pre-wire the light's
|
|
579
|
+
"view receipts →" link to it; if you mount by hand, wiring `receiptsHref` to
|
|
580
|
+
a room is YOUR one extra line — the light must never dead-end in raw JSON.
|
|
573
581
|
|
|
574
582
|
With a bundler, import straight from the npm package
|
|
575
583
|
(`import { mountTamperSignal } from "tamper-signal/light"`). Without one, copy
|
|
@@ -580,24 +588,34 @@ through `site-packages` or `node_modules`):
|
|
|
580
588
|
tamper-signal assets --out badge/ # Python; tamper-signal assets --out badge/ on Node
|
|
581
589
|
```
|
|
582
590
|
|
|
583
|
-
That writes `light.js`, `badge.js`, `element.js`, `table.js`,
|
|
584
|
-
into `badge/`. For the inline signal you need two of them side
|
|
585
|
-
imports `./badge.js` relatively)
|
|
591
|
+
That writes `light.js`, `badge.js`, `element.js`, `table.js`, `console.js`,
|
|
592
|
+
and `room.js` into `badge/`. For the inline signal you need two of them side
|
|
593
|
+
by side (light.js imports `./badge.js` relatively); the room needs `room.js`
|
|
594
|
+
and `badge.js`:
|
|
586
595
|
|
|
587
|
-
- `badge/badge.js` (verification core
|
|
596
|
+
- `badge/badge.js` (the shared verification core)
|
|
588
597
|
- `badge/light.js` (the signal: the inline status light)
|
|
598
|
+
- `badge/room.js` (the room: `mountSignalRoom`, `<tamper-signal-room>`)
|
|
589
599
|
|
|
590
600
|
Serve the `receipts/` directory statically, then mount the signal in the host
|
|
591
|
-
header
|
|
592
|
-
you vendored into `badge/` and serve it
|
|
601
|
+
header and a room for it to link to. Import the assets from wherever you
|
|
602
|
+
served them; the snippets here assume you vendored into `badge/` and serve it
|
|
603
|
+
at `/badge/`:
|
|
593
604
|
|
|
594
605
|
```html
|
|
595
606
|
<script type="module">
|
|
596
607
|
import { mountTamperSignal } from "/badge/light.js";
|
|
597
|
-
|
|
608
|
+
import { mountSignalRoom } from "/badge/room.js";
|
|
609
|
+
mountSignalRoom(document.querySelector("#data"), "/receipts/chain.json");
|
|
610
|
+
mountTamperSignal(document.querySelector("header"), "/receipts/chain.json",
|
|
611
|
+
undefined, { receiptsHref: "#tamper-room" });
|
|
598
612
|
</script>
|
|
599
613
|
```
|
|
600
614
|
|
|
615
|
+
(The first room in a document owns the id `tamper-room`, so `#tamper-room`
|
|
616
|
+
always reaches it. If the room lives on its own page, point `receiptsHref` at
|
|
617
|
+
that URL instead — the attach helpers do exactly this.)
|
|
618
|
+
|
|
601
619
|
**These surfaces verify over HTTP, not from `file://`.** The signal, badge, and
|
|
602
620
|
table all `fetch()` the chain (and table.json), which the browser blocks on a
|
|
603
621
|
`file://` page, so opening `index.html` directly leaves them silently
|
|
@@ -615,22 +633,30 @@ in the `exports` map), so imports like `tamper-signal/react`, `/table`, and
|
|
|
615
633
|
the Node entries (`.`, `/express`) expect `@types/node` as a normal Node
|
|
616
634
|
project already has.
|
|
617
635
|
|
|
618
|
-
Any other framework, or plain HTML: the web
|
|
636
|
+
Any other framework, or plain HTML: the web components. Import
|
|
619
637
|
`tamper-signal/element` (or vendor `badge/element.js`, which needs light.js
|
|
620
|
-
and badge.js beside it)
|
|
638
|
+
and badge.js beside it) for the light, `tamper-signal/room` (or
|
|
639
|
+
`badge/room.js`) for the room, and write one tag each:
|
|
621
640
|
|
|
622
641
|
```html
|
|
623
|
-
<tamper-signal chain="/receipts/chain.json"></tamper-signal>
|
|
642
|
+
<tamper-signal chain="/receipts/chain.json" receipts-href="#tamper-room"></tamper-signal>
|
|
643
|
+
<tamper-signal-room chain="/receipts/chain.json"></tamper-signal-room>
|
|
624
644
|
```
|
|
625
645
|
|
|
626
|
-
|
|
646
|
+
Light attributes mirror the options: `pub-key`, `watch`, `warn-drift`,
|
|
627
647
|
`receipts-href`, `surface` (`"light"` default or `"dark"` for a dark host),
|
|
628
648
|
`invert` (present = shortcut for `surface="dark"`); `theme` is the deprecated
|
|
629
|
-
alias of `surface="dark"`.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
649
|
+
alias of `surface="dark"`. Room attributes: `chain` (required), `table`,
|
|
650
|
+
`timeline`, `pub-key`, `watch`, `warn-drift`, `strict`, `max-rows`, `focus`,
|
|
651
|
+
`preset` (`room` | `table` | `console`), `density` (`embedded` | `page`).
|
|
652
|
+
React hosts use `<tamper-signal-room>` straight from JSX (`badge/room.d.ts`
|
|
653
|
+
ships the JSX typing) — import `tamper-signal/room` for the side effect.
|
|
654
|
+
|
|
655
|
+
Prefer the one-call attach helpers; each serves the receipts directory, the
|
|
656
|
+
bundled browser assets, AND the room page at `<assets_prefix>/receipts`, and
|
|
657
|
+
returns a `snippet` to render once in the layout (it mounts the signal into
|
|
658
|
+
`header`, falling back to `body`, with `receiptsHref` pre-wired to the served
|
|
659
|
+
room):
|
|
634
660
|
|
|
635
661
|
```python
|
|
636
662
|
# Flask
|
|
@@ -659,11 +685,19 @@ These verify SERVER-SIDE with the Python verifier and the pill says so;
|
|
|
659
685
|
Streamlit cannot serve the receipts directory for the in-browser walk, and
|
|
660
686
|
faking the stronger claim would violate rule 1.
|
|
661
687
|
|
|
662
|
-
Every attach helper
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
688
|
+
Every attach helper serves the room at `<assets_prefix>/receipts` (e.g.
|
|
689
|
+
`/tamper-signal/receipts`) and adds `?focus=auto` to the light's link, so a
|
|
690
|
+
red chain opens scrolled to the break exhibit and a yellow one to the first
|
|
691
|
+
caveat. The helper return exposes `roomUrl` and `roomSnippet` (an inline
|
|
692
|
+
embedded room, for a host that renders its own Data tab); `console_url` /
|
|
693
|
+
`consoleUrl` stays reachable, serving the same room with its rail open.
|
|
694
|
+
Opting out (`room=False` / `{ room: false }`) is not recommended; the light
|
|
695
|
+
will link to raw JSON. Mention the room to the user when handing over; it is
|
|
696
|
+
the page to open when the light is anything but green.
|
|
697
|
+
|
|
698
|
+
Hosts that want their own tab chrome: mount `roomSnippet` inside the tab,
|
|
699
|
+
pass `strict`, listen for the bubbling `tamper-signal:state` event, and paint
|
|
700
|
+
your own dot on your own tab — the room never touches host chrome.
|
|
667
701
|
|
|
668
702
|
Manual fallback when no helper fits: serve the directory statically (Flask
|
|
669
703
|
`static_folder="receipts"`, FastAPI `StaticFiles`, Express
|
|
@@ -679,8 +713,8 @@ Options on the fourth argument: `watch` (re-verify every N ms), `warnDrift`,
|
|
|
679
713
|
boolean shortcut for `surface: "dark"`); the old `theme: "light"` still works
|
|
680
714
|
as the `surface: "dark"` alias.
|
|
681
715
|
|
|
682
|
-
The expandable badge (`renderReceiptBadge
|
|
683
|
-
|
|
716
|
+
The expandable badge (`renderReceiptBadge`) is deprecated (removed in 3.0):
|
|
717
|
+
mount the light with the room behind it instead.
|
|
684
718
|
|
|
685
719
|
## 7. Let the signal flag broken metrics
|
|
686
720
|
|
|
@@ -701,66 +735,60 @@ so their column never reaches `numeric_sums`, and a `data-receipt-column` on it
|
|
|
701
735
|
can never flag a change — silently. Grouping isn't auto-stripped on purpose: it
|
|
702
736
|
would diverge from the Python canonicalization and is locale-ambiguous (`"1,234"`
|
|
703
737
|
is 1234 or 1.234?). Fix it upstream with a signed normalize stage that strips
|
|
704
|
-
the separators before the receipt is written. `tamper-signal ingest`
|
|
705
|
-
warning naming any such columns
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
The component re-hashes the served document in the viewer's browser and
|
|
759
|
-
compares it against the final receipt, so VERIFIED means the rows on screen
|
|
760
|
-
are byte-for-byte the attested data. It renders its own states: green, yellow
|
|
761
|
-
with caveats, chain broken (with the moved columns flagged), and "not the
|
|
762
|
-
attested data" when table.json is stale or edited. Design reference:
|
|
763
|
-
`designs/03-data-tab.html`.
|
|
738
|
+
the separators before the receipt is written. On Node, `tamper-signal ingest`
|
|
739
|
+
prints a warning naming any such columns, and `groupedNumericColumns(records)`
|
|
740
|
+
finds them in code; the Python CLI also warns on stderr during normal ingest
|
|
741
|
+
(both CLIs keep `--json` stderr silent), and `grouped_numeric_columns(records)`
|
|
742
|
+
is available from `tamper_signal`.
|
|
743
|
+
|
|
744
|
+
## 8. Publish table.json so the room's landing plane fills
|
|
745
|
+
|
|
746
|
+
The room's primary content is the attested data table. It fills from
|
|
747
|
+
`table.json`, the canonical table document, which the room re-hashes in the
|
|
748
|
+
viewer's browser against the final receipt — so its VERIFIED means the rows
|
|
749
|
+
on screen are byte-for-byte the attested data. Export it as the LAST step of
|
|
750
|
+
every pipeline run:
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
# Python
|
|
754
|
+
tamper-signal export receipts/chain.json --data path/to/dashboard_data.xlsx
|
|
755
|
+
# JavaScript
|
|
756
|
+
tamper-signal export receipts/chain.json --data path/to/dashboard_data.csv
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
The chain path is positional in both CLIs (the Python CLI also accepts
|
|
760
|
+
`--chain receipts/chain.json` for the same value). The command refuses if the
|
|
761
|
+
data does not match the final receipt — the room only ever shows attested
|
|
762
|
+
data. In a JS build you can write it programmatically instead with
|
|
763
|
+
`canonicalDocument(finalRecords)` (see step 1b).
|
|
764
|
+
|
|
765
|
+
Skipping or forgetting this step degrades honestly, never silently:
|
|
766
|
+
|
|
767
|
+
- **No table.json published**: the room's table plane shows a grey "NO
|
|
768
|
+
ATTESTED TABLE PUBLISHED" slab naming the export command. The chain
|
|
769
|
+
verdict, rail, and drawers still render; grey, because absence is not
|
|
770
|
+
tampering. The emitted state carries `attested: false`.
|
|
771
|
+
- **Stale table.json** (pipeline re-ran, export didn't): the room's verdict
|
|
772
|
+
reads `NOT THE ATTESTED DATA` with the exact re-run command — a
|
|
773
|
+
build-behind state, deliberately distinct from a broken chain.
|
|
774
|
+
|
|
775
|
+
So: wire the export into the pipeline run itself, not a manual step. Both
|
|
776
|
+
wrappers do it for you — pass `exportTable: true` to `rebuildChain(...)`
|
|
777
|
+
(Node), or `write_table=True` on your FINAL `@receipt_step` stage (Python) —
|
|
778
|
+
and `table.json` is written as the last pipeline step, always matching the
|
|
779
|
+
chain tail. If a published table does go stale anyway, `tamper-signal verify`
|
|
780
|
+
prints a one-line stderr reminder naming the re-run command (absence stays
|
|
781
|
+
silent: CLI-only projects never publish a table).
|
|
782
|
+
|
|
783
|
+
Embedding the room inline (instead of the helper-served page): mount
|
|
784
|
+
`mountSignalRoom` / `<tamper-signal-room>` where the old Data tab lived. The
|
|
785
|
+
`table.js` shim (`mountReceiptTable`, `<tamper-signal-table>`) keeps working
|
|
786
|
+
through 2.x and now renders the room's table preset. The state contract is
|
|
787
|
+
unchanged: after each verification the room fires a bubbling
|
|
788
|
+
`tamper-signal:state` event (and an `onState` callback) carrying
|
|
789
|
+
`{ state, attested, strict }`, where `state` is the chain verdict and
|
|
790
|
+
`attested` the byte-identity boolean. The room never blocks UI itself.
|
|
791
|
+
Recommended host gate: `strict && (state === "red" || !attested)`.
|
|
764
792
|
|
|
765
793
|
## 9. Verify your work before reporting done
|
|
766
794
|
|
|
@@ -776,14 +804,25 @@ gitignored yourself.) Then confirm the user-visible surfaces:
|
|
|
776
804
|
1. `tamper-signal verify receipts/chain.json --pub keys/signing.pub` exits 0.
|
|
777
805
|
2. Load the host page: the pill reads `VERIFIED · chain intact` (click it for
|
|
778
806
|
the per-stage popover).
|
|
779
|
-
3.
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
4.
|
|
807
|
+
3. Click the pill's `view receipts →` link and confirm it lands in the room
|
|
808
|
+
(the served page or an inline mount), NOT on raw chain.json. If it opens
|
|
809
|
+
unstyled JSON, `receiptsHref` was never wired — fix it (the attach helpers
|
|
810
|
+
do it for you).
|
|
811
|
+
4. Confirm the room's table plane shows the attested rows with the signed
|
|
812
|
+
control-totals row (not the grey "no attested table published" slab). If
|
|
813
|
+
it shows the slab, the export step (§8) is missing from the pipeline run.
|
|
814
|
+
5. Negative test without touching the user's real chain: this repo commits
|
|
815
|
+
known-good fixtures under `examples/chains/`. Point the room at
|
|
816
|
+
`tampered/` and confirm the break exhibit leads with business numbers;
|
|
817
|
+
point it at `gap/` and confirm the located caveat card and the ghost node
|
|
818
|
+
in the expanded rail.
|
|
819
|
+
6. If the pill reads `UNVERIFIED · could not load chain`, the receipts
|
|
784
820
|
directory is not being served at the URL you passed (or it is blocked by
|
|
785
821
|
CORS). That state is a capability fallback, not a verdict.
|
|
786
822
|
|
|
823
|
+
When you report done, tell the user where the docs and issue tracker live:
|
|
824
|
+
https://github.com/welovejeff/tamper-evident-verification.
|
|
825
|
+
|
|
787
826
|
## Troubleshooting
|
|
788
827
|
|
|
789
828
|
- `ChainTailMismatch` when running a wrapped transform: the data fed to the
|
|
@@ -802,8 +841,9 @@ gitignored yourself.) Then confirm the user-visible surfaces:
|
|
|
802
841
|
|---|---|
|
|
803
842
|
| `tamper_signal/` | Python package: canonicalization, keys, receipts, verify, `receipt_step` |
|
|
804
843
|
| `node/` | JavaScript package: same API (`receiptStep`, `verifyChain`), interchangeable chains |
|
|
805
|
-
| `badge/badge.js` | Browser verification core
|
|
844
|
+
| `badge/badge.js` | Browser verification core (shared by every surface) |
|
|
806
845
|
| `badge/light.js` | The signal (inline status light), `mountTamperSignal` |
|
|
846
|
+
| `badge/room.js` | The room (the one robust surface), `mountSignalRoom`, `<tamper-signal-room>` |
|
|
807
847
|
| `badge/light-react.js` | `<TamperSignal />` React wrapper |
|
|
808
848
|
| `examples/chains/` | Committed known-good and known-broken demo chains |
|
|
809
849
|
| `designs/` | Working HTML mockups for the signal, console, and Data tab |
|
package/README.md
CHANGED
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
# The light is green, the data is clean.
|
|
4
4
|
|
|
5
|
-
[](https://pypi.org/project/tamper-signal/) [](https://www.npmjs.com/package/tamper-signal) [](https://pypi.org/project/tamper-signal/) [](https://www.npmjs.com/package/tamper-signal) [](https://github.com/welovejeff/tamper-evident-verification/actions/workflows/test.yml) [](https://socket.dev/npm/package/tamper-signal/overview/2.1.1) [](https://socket.dev/pypi/package/tamper-signal/overview/2.1.1) [](https://github.com/welovejeff/tamper-evident-verification/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Tamper Signal is an open-source Python and JavaScript library and CLI that signs a receipt at every stage of a data pipeline and verifies the chain as a green, yellow, or red light: lightweight data provenance for export-to-dashboard pipelines. It can't tell you the data is right, but it can prove nobody changed it.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+

|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
*The inline status light: a small dark instrument in your dashboard's header. When the chain breaks, it reaches into the page and flags the exact metric that no longer descends from the source.*
|
|
12
|
+
|
|
13
|
+
**Try it in your browser, no install:** [tampersignal.com/demo.html](https://tampersignal.com/demo.html) runs every surface on a real receipt chain; flip it to tampered and watch the light catch it. Or `pip install tamper-signal` / `npm install tamper-signal`.
|
|
14
|
+
|
|
15
|
+
**Or run it yourself in a notebook:** [](https://colab.research.google.com/github/welovejeff/tamper-evident-verification/blob/main/examples/quickstart.ipynb) builds a two-stage pandas pipeline with receipts, then shows green, yellow, and red on real output ([`examples/quickstart.ipynb`](examples/quickstart.ipynb)).
|
|
16
|
+
|
|
17
|
+
If this is useful, a star helps other people with AI-built dashboards find it.
|
|
18
|
+
|
|
19
|
+
**Pointing a coding agent at this repo?** `AGENTS.md` is the full integration runbook: install, keygen, ingest, wrap transforms, mount the signal, verify. Tell your agent "add tamper signal" and it will find it. In Claude Code, `/plugin marketplace add welovejeff/tamper-evident-verification` then `/plugin install tamper-signal@welovejeff` adds an `add-tamper-signal` skill that does the same.
|
|
12
20
|
|
|
13
21
|
## The problem
|
|
14
22
|
|
|
23
|
+
Your social team exports a month of TikTok performance data. Someone vibe-codes a dashboard on top of it with an AI assistant in an afternoon. It looks great. Then a transform silently drops 22 rows, or someone hand-edits a file between steps, and the numbers in front of your boss are wrong. Nothing in that workflow catches it. Tamper Signal is the missing verification layer: every stage of the pipeline signs a receipt for what went in and what came out, so a dropped row shows up in that stage's own totals, and one command (or a light on the dashboard itself) tells you whether the chain is intact, or exactly where it broke and by how much.
|
|
24
|
+
|
|
15
25
|
Vibe-coded pipelines fail silently. AI-generated transform scripts work most of the time, and when they don't, they don't crash. They drop rows. They double-count. They coerce a column wrong and quietly shift every total. The dashboard still renders. The chart still looks plausible. Nobody re-checks 48,000 rows by hand.
|
|
16
26
|
|
|
17
27
|
Traditional answers (warehouse lineage, dbt, data-quality suites) assume infrastructure a small team running xlsx-to-dashboard doesn't have. This is the lightweight version: signed receipts as files on disk, no database, no server, no catalog.
|
|
@@ -24,11 +34,11 @@ The badge and the verifier reduce the whole chain to one state:
|
|
|
24
34
|
- 🟡 **Yellow.** Verifiable, but with caveats: gaps in receipt coverage, an unrecognized signing key, or control-total drift that needs a human look.
|
|
25
35
|
- 🔴 **Red.** Chain broken. A hash doesn't match at a specific link. You get the exact stage and the control-totals delta (e.g. `row_count 48212 -> 48190 (-22)`).
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
Honest status: all three verdicts are implemented in `tamper-signal verify` and the browser surfaces. Yellow today covers two detectable caveats (a coverage gap in the receipt numbering, and signatures that only verify under the chain's embedded key rather than the key you trust) plus opt-in control-total drift via `--warn-drift`. The animations in this README are renders of the design mockups in `designs/`; the interfaces they depict have since shipped and, as of 2.1, unified. The surfaces also render a separate grey state ("could not load" or "verification unsupported in this browser"); that is a capability fallback that says nothing about the chain, not the yellow verdict.
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
## One light, one room
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
The browser UI is two things, always shipped together. **The light** (`badge/light.js`) is the whole footprint on your dashboard: a small dark pill in the header. **The room** (`badge/room.js`, since 2.1) is the one surface behind it, where the pill's "view receipts →" lands: the attested data table as the landing plane (re-hashed in the viewer's browser against the final receipt), with the chain as a provenance rail, the break exhibit in business numbers when something is wrong, and the receipt inspector, CLI-mirror event log, chain-of-custody timeline, and "Take your data" evidence export one drawer away. Green earns silence; the layout leads with whatever the verdict demands. The attach helpers serve the room automatically and wire the light to it, so the default integration is both halves in one call. (`tamper-signal/table` and `tamper-signal/console` from 2.0 keep working as room presets.)
|
|
32
42
|
|
|
33
43
|
## 60-second quickstart
|
|
34
44
|
|
|
@@ -36,11 +46,10 @@ Python 3.11+. Open source (MIT).
|
|
|
36
46
|
|
|
37
47
|
```bash
|
|
38
48
|
pip install tamper-signal
|
|
39
|
-
git clone https://github.com/welovejeff/tamper-evident-verification && cd tamper-evident-verification
|
|
40
49
|
tamper-signal demo
|
|
41
50
|
```
|
|
42
51
|
|
|
43
|
-
`tamper-signal demo` runs the whole story end to end: generates a deliberately messy sample export, ingests it, runs two AI-written-style transforms, verifies the chain (PASS), then tampers with one spend value and verifies again (FAIL, pinpointing the
|
|
52
|
+
`tamper-signal demo` runs the whole story end to end: generates a deliberately messy sample export, ingests it, runs two AI-written-style transforms, verifies the chain (PASS), then tampers with one spend value and verifies again (FAIL, pinpointing the totals delta). It works in its own `tamper-signal-demo/` folder, so your own keys and receipts are never touched. Run it from a clone of the repo (`git clone https://github.com/welovejeff/tamper-evident-verification && cd tamper-evident-verification`) and it finishes by serving the badge at `http://localhost:8000/badge/badge.html` so you can see green, yellow, and red side by side.
|
|
44
53
|
|
|
45
54
|
> **`tamper-signal: command not found`?** pip installed the script into a bin directory that is not on PATH (common on the python.org framework Python, the default macOS download). Either run it through the same interpreter, `python3 -m tamper_signal verify ...` (works as a drop-in for every `tamper-signal ...` command), or link it onto PATH once: `sudo ln -sf "$(python3 -c 'import sysconfig;print(sysconfig.get_path("scripts"))')/tamper-signal" /usr/local/bin/tamper-signal`.
|
|
46
55
|
|
|
@@ -54,7 +63,7 @@ tamper-signal diff # compare two runs: code-hash changes and tot
|
|
|
54
63
|
tamper-signal log # archived run history as a per-metric trend across runs (read-only)
|
|
55
64
|
tamper-signal doctor # integration self-check with actionable fixes
|
|
56
65
|
tamper-signal serve # serve receipts/ on localhost with CORS (dev only)
|
|
57
|
-
tamper-signal assets --out badge/ # vendor the browser surfaces (light/badge/element/table/console.js) into a project
|
|
66
|
+
tamper-signal assets --out badge/ # vendor the browser surfaces (light/badge/element/table/console/room.js) into a project
|
|
58
67
|
tamper-signal annotate --reason "backfill approved" --author dana # sign a reason onto a receipt (chain of custody)
|
|
59
68
|
tamper-signal watch --config feed.json --out receipts/ # poll a live feed onto the chain (needs [watch]; see below)
|
|
60
69
|
```
|
|
@@ -89,7 +98,7 @@ const clean = receiptStep(
|
|
|
89
98
|
const output = await clean(loadCsv("export.csv"));
|
|
90
99
|
```
|
|
91
100
|
|
|
92
|
-
JavaScript reads .csv, .tsv, .json, and .ndjson; spreadsheets go through the Python CLI. The browser surfaces ship in the same package: `tamper-signal/light`, `tamper-signal/
|
|
101
|
+
JavaScript reads .csv, .tsv, .json, and .ndjson; spreadsheets go through the Python CLI. The browser surfaces ship in the same package: `tamper-signal/light`, `tamper-signal/room`, `tamper-signal/element`, `tamper-signal/react` (plus the 2.0 `tamper-signal/table` and `tamper-signal/console`, now room presets).
|
|
93
102
|
|
|
94
103
|
## How the chain works
|
|
95
104
|
|
|
@@ -128,7 +137,7 @@ Hashes say "broken." Totals say "how broken."
|
|
|
128
137
|
|
|
129
138
|
`badge/badge.js` exports `renderReceiptBadge(containerEl, chainUrl, pubKeyHex)`. Drop it into any web frontend, point it at your `receipts/chain.json`, and it re-verifies the whole chain client-side with Web Crypto Ed25519: every signature, every hash link. No build step, no framework, no server-side trust. The badge re-checks hash links only; it does not re-canonicalize xlsx in the browser.
|
|
130
139
|
|
|
131
|
-

|
|
140
|
+

|
|
132
141
|
|
|
133
142
|
Green collapsed state reads like: `✓ Verified · TikTok export, May 2026 · 48,212 rows · 2 transforms · chain intact`. Expanding shows one row per receipt.
|
|
134
143
|
|
|
@@ -157,7 +166,7 @@ We think any dashboard built on verified data should let you see the data. Not a
|
|
|
157
166
|
|
|
158
167
|
It ships: `tamper-signal export` writes the canonical table document next to the chain (refusing data that does not match the final receipt), and `mountReceiptTable(el, "/receipts/chain.json")` from `badge/table.js` (npm: `tamper-signal/table`) renders it after re-hashing it in the viewer's browser against the final receipt. VERIFIED means the rows on screen are byte-for-byte the attested data; a stale or edited table.json renders dimmed under a "not the attested data" strip, and a broken chain flags the columns that moved at the break. Live demo: `badge/table.html`.
|
|
159
168
|
|
|
160
|
-

|
|
169
|
+

|
|
161
170
|
|
|
162
171
|
*Design preview: install the verification layer and your dashboard grows a Data tab. When the chain breaks, the break is localized to the column and total that no longer verify, right in the table.*
|
|
163
172
|
|
|
@@ -171,15 +180,15 @@ To bring an updated file back, `tamper-signal ingest --as replace|period`. `repl
|
|
|
171
180
|
|
|
172
181
|
The light answers "is it fine?"; the console answers "where, exactly, and by how much?" `mountReceiptConsole(el, "/receipts/chain.json")` from `badge/console.js` (npm: `tamper-signal/console`) renders the chain as an inspectable pipeline: links carry the hash they proved, a break severs the link with the break card pinned at it, coverage gaps appear as ghost nodes at their position, and the event log mirrors `tamper-signal verify` line for line. Every attach helper also serves it ready-made at `/tamper-signal/console`. Live demo: `badge/console.html`.
|
|
173
182
|
|
|
174
|
-

|
|
183
|
+

|
|
175
184
|
|
|
176
185
|
*The verification console: calm when green, surgical when red.*
|
|
177
186
|
|
|
178
|
-
Below the pipeline the console renders the **chain of custody**: the imports and changes, each signed reason attached to the receipt it explains (`tamper-signal annotate`), and any changes **awaiting human review**. It is an additive layer over the published `timeline.json
|
|
187
|
+
Below the pipeline the console renders the **chain of custody**: the imports and changes, each signed reason attached to the receipt it explains (`tamper-signal annotate`), and any changes **awaiting human review**. It is an additive layer over the published `timeline.json`; it never feeds the verdict above.
|
|
179
188
|
|
|
180
189
|
## Live-source watcher (optional)
|
|
181
190
|
|
|
182
|
-
When the source is a live feed rather than a file you re-export by hand, the watcher keeps it on the same signed chain. `tamper-signal watch` (behind `pip install "tamper-signal[watch]"`) polls an HTTP/JSON-API or RSS/Atom endpoint, judges the new data against the declared band/settle, and **auto-appends only a clean change**. A retroactive edit to an already-settled period
|
|
191
|
+
When the source is a live feed rather than a file you re-export by hand, the watcher keeps it on the same signed chain. `tamper-signal watch` (behind `pip install "tamper-signal[watch]"`) polls an HTTP/JSON-API or RSS/Atom endpoint, judges the new data against the declared band/settle, and **auto-appends only a clean change**. A retroactive edit to an already-settled period, or a slow drift that cumulatively breaches the band, is never signed unattended: it is withheld as a signed *pending event* and paused for a human.
|
|
183
192
|
|
|
184
193
|
```bash
|
|
185
194
|
pip install "tamper-signal[watch]"
|
|
@@ -188,7 +197,7 @@ tamper-signal review #
|
|
|
188
197
|
tamper-signal review accept <hash> --reason "confirmed by finance" # sign off + commit
|
|
189
198
|
```
|
|
190
199
|
|
|
191
|
-
The fetch is SSRF-hardened (public hosts only, redirects off, TLS verified, byte + wall-clock caps; RSS parsed through `defusedxml`), change detection uses a full-content fingerprint rather than a trust-me `ETag`, and the unattended commit is crash-safe. The recommended deployment is the stateless tick under a systemd timer or cron, so the signing key is not resident between runs
|
|
200
|
+
The fetch is SSRF-hardened (public hosts only, redirects off, TLS verified, byte + wall-clock caps; RSS parsed through `defusedxml`), change detection uses a full-content fingerprint rather than a trust-me `ETag`, and the unattended commit is crash-safe. The recommended deployment is the stateless tick under a systemd timer or cron, so the signing key is not resident between runs (see `AGENTS.md` §5c for a hardened unit). `watch`/`review` are Python-only today; the chains they write are read and verified by the JavaScript stack unchanged.
|
|
192
201
|
|
|
193
202
|
## Anchoring (optional)
|
|
194
203
|
|
|
@@ -211,4 +220,4 @@ Those tools model lineage and quality at the warehouse and orchestration layer.
|
|
|
211
220
|
|
|
212
221
|
## Contributing
|
|
213
222
|
|
|
214
|
-
Open source under the MIT license (see `LICENSE`), designed to be added to any vibe-coded data project. The Python package is in `tamper_signal/`, tests in `tests/` (run `pytest`), examples in `examples/`, the badge in `badge/`. Issues and
|
|
223
|
+
Open source under the MIT license (see `LICENSE`), designed to be added to any vibe-coded data project. The Python package is in `tamper_signal/`, tests in `tests/` (run `pytest`), examples in `examples/`, the badge in `badge/`. Issues, PRs, and stars all help; [CONTRIBUTING.md](https://github.com/welovejeff/tamper-evident-verification/blob/main/CONTRIBUTING.md) covers setup and the few rules that keep both stacks in step. The original Luhn hash demo lives unchanged in `legacy/` and is off the main path.
|