diff --git a/.gitignore b/.gitignore index dad73ecbb..f783a9889 100644 --- a/.gitignore +++ b/.gitignore @@ -85,11 +85,11 @@ secrets.json # Artifacts that should never be tracked *.zip -# ...except the public proof bundles the frontend serves. These are -# intentionally tracked fixtures (the audit-bundle pair backs the public -# /api/v1/proof/verify demo, including the deliberately tampered one), and +# ...except the proof-verify test fixtures. These are +# intentionally tracked fixtures (the audit-bundle pair backs the +# /api/v1/proof/verify tests, including the deliberately tampered one), and # the bare *.zip rule above was silently covering them (#2873). -!frontend/public/proof/*.zip +!services/ingestion/tests/fixtures/proof/*.zip login_resp.json frontend/build_log.txt frontend/build_output.txt diff --git a/frontend/public/proof/epcis-sample.jsonld b/frontend/public/proof/epcis-sample.jsonld deleted file mode 100644 index c107be48a..000000000 --- a/frontend/public/proof/epcis-sample.jsonld +++ /dev/null @@ -1,88 +0,0 @@ -{ - "_comment": "SYNTHETIC — Sample EPCIS 2.0 JSON-LD payload for the RegEngine /proof page. Three Critical Tracking Events covering harvesting, initial packing, and shipping for a single lot of romaine lettuce. Modeled on services/ingestion/app/epcis/. Not a real grower, lot, or shipment.", - "@context": [ - "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld", - { - "fsma": "urn:fsma:traceability:" - } - ], - "type": "EPCISDocument", - "schemaVersion": "2.0", - "creationDate": "2026-02-08T17:00:10Z", - "epcisBody": { - "eventList": [ - { - "type": "ObjectEvent", - "eventTime": "2026-02-05T13:42:00Z", - "eventTimeZoneOffset": "-08:00", - "epcList": [ - "urn:epc:id:sgtin:0614141.107346.SV-20240115-001" - ], - "action": "ADD", - "bizStep": "urn:fsma:traceability:harvesting", - "disposition": "urn:epcglobal:cbv:disp:active", - "readPoint": { - "id": "urn:epc:id:sgln:0614141.00001.0" - }, - "bizLocation": { - "id": "urn:epc:id:sgln:0614141.00001.0" - }, - "fsma:traceabilityLotCode": "SV-20240115-001", - "fsma:foodCommodity": "Romaine Lettuce, fresh-cut", - "fsma:harvestDate": "2026-02-05", - "fsma:growingArea": "Field 7B, Salinas Valley, CA" - }, - { - "type": "ObjectEvent", - "eventTime": "2026-02-06T08:15:00Z", - "eventTimeZoneOffset": "-08:00", - "epcList": [ - "urn:epc:id:sgtin:0614141.107346.SV-20240115-001" - ], - "action": "OBSERVE", - "bizStep": "urn:fsma:traceability:initial_packing", - "disposition": "urn:epcglobal:cbv:disp:in_progress", - "readPoint": { - "id": "urn:epc:id:sgln:0614141.00002.0" - }, - "bizLocation": { - "id": "urn:epc:id:sgln:0614141.00002.0" - }, - "fsma:traceabilityLotCode": "SV-20240115-001", - "fsma:packDate": "2026-02-06", - "fsma:packingFacility": "Salinas Valley Cooling & Pack, Salinas, CA", - "fsma:quantity": { - "value": 480, - "uom": "case" - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-02-07T05:30:00Z", - "eventTimeZoneOffset": "-08:00", - "epcList": [ - "urn:epc:id:sgtin:0614141.107346.SV-20240115-001" - ], - "action": "OBSERVE", - "bizStep": "urn:fsma:traceability:shipping", - "disposition": "urn:epcglobal:cbv:disp:in_transit", - "readPoint": { - "id": "urn:epc:id:sgln:0614141.00002.0" - }, - "bizLocation": { - "id": "urn:epc:id:sgln:0614141.00002.0" - }, - "destinationList": [ - { - "type": "urn:epcglobal:cbv:sdt:location", - "destination": "urn:epc:id:sgln:0614141.00099.0" - } - ], - "fsma:traceabilityLotCode": "SV-20240115-001", - "fsma:shipDate": "2026-02-07", - "fsma:shipToEntity": "DC-099, Tracy, CA", - "fsma:carrier": "Reefer Logistics Inc." - } - ] - } -} diff --git a/frontend/public/proof/fsma-204-sample.csv b/frontend/public/proof/fsma-204-sample.csv deleted file mode 100644 index daa15cd42..000000000 --- a/frontend/public/proof/fsma-204-sample.csv +++ /dev/null @@ -1,11 +0,0 @@ -Traceability Lot Code (TLC),Product Description,Quantity,Unit of Measure,Event Type,Event Date,Event Time,Facility Name,Facility GLN,Facility Address,Confidence Score -SV-20240115-001,Romaine Lettuce,500,cases,SHIPPING,2024-01-15,14:00:00,Sunny Valley Farms,1234567890128,"123 Farm Road, Salinas CA",0.98 -SV-20240115-001,Romaine Lettuce,500,cases,RECEIVING,2024-01-16,08:00:00,Fresh Cut Co,2345678901234,"456 Processing Lane, Fresno CA",0.97 -FC-20240116-A,Chopped Romaine,250,cases,TRANSFORMATION,2024-01-16,10:00:00,Fresh Cut Co,2345678901234,"456 Processing Lane, Fresno CA",0.95 -FC-20240116-B,Romaine Hearts,250,cases,TRANSFORMATION,2024-01-16,11:00:00,Fresh Cut Co,2345678901234,"456 Processing Lane, Fresno CA",0.95 -FC-20240116-A,Chopped Romaine,100,cases,SHIPPING,2024-01-17,06:00:00,Fresh Cut Co,2345678901234,"456 Processing Lane, Fresno CA",0.96 -FC-20240116-A,Chopped Romaine,100,cases,RECEIVING,2024-01-17,10:00:00,Metro Foods,3456789012340,"789 Distribution Dr, Oakland CA",0.94 -FC-20240116-B,Romaine Hearts,150,cases,SHIPPING,2024-01-17,07:00:00,Fresh Cut Co,2345678901234,"456 Processing Lane, Fresno CA",0.96 -FC-20240116-B,Romaine Hearts,150,cases,RECEIVING,2024-01-17,11:00:00,Valley Wholesale,4567890123456,"321 Wholesale Ave, Stockton CA",0.93 -FC-20240116-A,Chopped Romaine,50,cases,SHIPPING,2024-01-18,05:00:00,Metro Foods,3456789012340,"789 Distribution Dr, Oakland CA",0.95 -FC-20240116-A,Chopped Romaine,50,cases,RECEIVING,2024-01-18,08:00:00,SuperMart #42,5678901234562,"100 Retail St, San Jose CA",0.92 diff --git a/frontend/src/app/proof/page.tsx b/frontend/src/app/proof/page.tsx index d6ef18bcc..d7aab73a0 100644 --- a/frontend/src/app/proof/page.tsx +++ b/frontend/src/app/proof/page.tsx @@ -1,250 +1,70 @@ import type { Metadata } from "next"; -import Link from "next/link"; -import { ArrowRight, Download, FileJson, FileSpreadsheet, Hash, Terminal } from "lucide-react"; +import { FileArchive, Terminal } from "lucide-react"; import { Container } from "@/components/layout/container"; import { PageHeader } from "@/components/layout/page-header"; import { Section } from "@/components/layout/section"; import { Button } from "@/components/ui/button"; -import { Card, CardHeader } from "@/components/ui/card"; +import { EmptyState } from "@/components/ui/empty-state"; export const metadata: Metadata = { title: "Proof | RegEngine", description: - "Download three verifiable sample artifacts generated by production code (FSMA 204 sortable spreadsheet, EPCIS 2.0 JSON-LD export (Preview), and a Merkle-rooted audit bundle) without signing up.", + "Inspect the open-source Python verifier RegEngine uses to check audit bundles. Exports include FSMA 204 sortable spreadsheets and EPCIS 2.0 JSON-LD (Preview).", openGraph: { title: "Verify RegEngine without signing up", description: - "Three downloadable artifacts: FSMA 204 spreadsheet, EPCIS 2.0 JSON-LD (Preview), and a Merkle-rooted audit bundle.", + "Inspect the open-source Python verifier RegEngine uses to check audit bundles.", url: "https://regengine.co/proof", type: "website", }, }; -type Artifact = { - name: string; - href: string; - filename: string; - sizeBytes: number; - icon: typeof FileSpreadsheet; - summary: string; - why: string; - formatLabel: string; -}; - -const ARTIFACTS: Artifact[] = [ - { - name: "FSMA 204 sortable spreadsheet", - href: "/proof/fsma-204-sample.csv", - filename: "fsma-204-sample.csv", - sizeBytes: 1524, - icon: FileSpreadsheet, - formatLabel: "CSV · sortable", - summary: - "A recall-lookback sample for one leafy-greens lot, exported in an FDA-oriented sortable layout.", - why: "FDA records requests may require records, including an electronic sortable spreadsheet under 21 CFR 1.1455(c)(3), unless an exemption or agreed alternative applies. This sample shows RegEngine's current column mapping and should be reviewed against the specific FDA request.", - }, - { - name: "EPCIS 2.0 JSON-LD export (Preview)", - href: "/proof/epcis-sample.jsonld", - filename: "epcis-sample.jsonld", - sizeBytes: 3039, - icon: FileJson, - formatLabel: "EPCIS 2.0 JSON-LD (Preview)", - summary: - "A three-event payload covering harvesting, initial packing, and shipping for one traceability lot code.", - why: "Several major buyers are standardizing on EPCIS 2.0 for supplier ingest. Confirm that what RegEngine emits validates against your retailer's onboarding tests before you sign a contract.", - }, - { - name: "Merkle-rooted audit bundle", - href: "/proof/audit-bundle.zip", - filename: "audit-bundle.zip", - sizeBytes: 2266, - icon: Hash, - formatLabel: "ZIP · SHA-256 + Merkle root", - summary: - "The CSV and EPCIS (Preview) payload plus a manifest.json with per-file SHA-256 hashes and a Merkle root computed by production code.", - why: "Compare each declared file's SHA-256 hash and the bundle's Merkle root with its included manifest. These checks establish internal consistency; they do not authenticate the issuer or establish that the source data is accurate.", - }, -]; - -function formatSize(bytes: number): string { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; -} - -/** - * A terminal transcript. SPEC §5.1: code is a well - one surface step below the - * card, bounded by a hairline rather than an inverted ink block. Result lines - * keep a status colour, but never alone: each one carries its own word ("ok", - * "matched", "FAILED"), so the meaning survives greyscale. - */ -function Transcript({ title, meta, children }: { title: string; meta?: string; children: React.ReactNode }) { - return ( - - -

{title}

- {meta &&

{meta}

} -
-
- {children} -
-
- ); -} - export default function ProofPage() { return (
- -
- Proof} - title="Verify RegEngine without signing up." - description="Three artifacts. No email gate, no demo call. Download the files, open them in your tools, and check the hashes yourself. If the output does not match the claim, the product fails, and you have the receipts." - className="mb-0 border-b-0 pb-0" - /> -
- - -
-
- -

$ unzip -p audit-bundle.zip manifest.json | jq .merkle.root

-

"a320428f40579ec9eaea1a640614d26649f2111a0cc6f6b379003d25278bb6d2"

-

$ python verify_chain.py audit-bundle.zip

-

manifest: ok

-

files: 2 / 2 sha256 matches

-

merkle: root recomputed and matched

-

result: bundle intact

-
+ + Proof} + title="Verify RegEngine without signing up." + description="No email gate, no sales call. Read the verifier, run it on a bundle exported from your own workspace, and check the hashes yourself." + className="mb-0 border-b-0 pb-0" + />
Downloads} - title="Three artifacts. Open them in your own tools." + title="Public artifacts" > -
- {ARTIFACTS.map(({ name, href, filename, sizeBytes, icon: Icon, summary, why, formatLabel }) => ( -
- - -
- ))} -
+
-
-
-

Run it yourself

-

- Inspect the Python verifier. -

-

- verify_chain.py reads the manifest, recomputes each declared file's SHA-256, and rebuilds the Merkle root using the same algorithm RegEngine ships in services/shared/merkle_tree.py. If those values disagree with the included manifest, the script exits non-zero. -

- -
- -

# 1. Download the bundle and the verifier

-

curl -O https://regengine.co/proof/audit-bundle.zip

-

curl -O https://regengine.co/sdk/verify_chain.py

-

# 2. Run it

-

python verify_chain.py audit-bundle.zip

-

# 3. (Optional) Recompute the leaf yourself

-

shasum -a 256 fsma-204-sample.csv

-
-
-
- -
-
-
-

Watch it fail

-

- A tampered bundle is published next to the real one. -

-

- audit-bundle-tampered.zip contains a changed CSV while the manifest's declared hash remains unchanged. Run the verifier against it and watch it exit non-zero. This demonstrates detection of changed file bytes against an unchanged manifest; replacing both the file and manifest is outside this check. -

- -
- -

$ python verify_chain.py audit-bundle-tampered.zip

-

manifest: ok

-

verify: FAILED: fsma-204-sample.csv sha256 mismatch

-

result: bundle TAMPERED

-

$ echo $?

-

1

-
-
-
- -
Changelog}> - -

2026-05-09: verifier mismatch found and fixed

-

- A reader running the recipe on this page got an argparse error instead of a verification. The verifier we'd shipped didn't match the bundle we'd shipped. The hashes were correct, but the script next to them read JSON, not the zip the page describes. -

-

- Replaced with a verifier that opens the zip, recomputes every per-file SHA-256, and rebuilds the Merkle root using the same algorithm RegEngine ships in services/shared/merkle_tree.py. Added the EPCIS payload to manifest.json, added a build block (commit, generated_at, release), and published audit-bundle-tampered.zip so anyone can watch the new verifier fail closed. +

+

Run it yourself

+

+ Inspect the Python verifier. +

+

+ verify_chain.py reads the manifest, recomputes each declared file's SHA-256, and rebuilds the Merkle root using the same algorithm RegEngine ships in services/shared/merkle_tree.py. If those values disagree with the included manifest, the script exits non-zero.

- If you found the original bug before we did, thank you. That's the system working. + These checks establish internal consistency; they do not authenticate the issuer or establish that the source data is accurate.

- + +
- -
- - - All artifacts are synthetic. They are regenerated by production code on every release. - - - python verify_chain.py audit-bundle.zip - - -
); } diff --git a/scripts/build_audit_bundle.sh b/scripts/build_audit_bundle.sh deleted file mode 100755 index 97f61042d..000000000 --- a/scripts/build_audit_bundle.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -# Build the public /proof audit bundle ZIP. -# -# Packages: -# - fsma-204-sample.csv (the FSMA 204 sortable sample) -# - manifest.json (per-file SHA-256 + Merkle root over those leaves) -# -# Merkle algorithm matches services/shared/merkle_tree.py: -# leaf = sha256("leaf|" + sha256(file_bytes).hexdigest()) -# node = sha256(left + "|" + right) -# odd-leaf-out: duplicate the last leaf (standard Merkle padding) -# single-leaf tree: root = leaf hash -# -# Usage: -# scripts/build_audit_bundle.sh -# -# Output: frontend/public/proof/audit-bundle.zip -# -# The companion sample-regenerator PR can invoke this to refresh the bundle -# whenever the underlying CSV is regenerated from production code. - -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -PROOF_DIR="$ROOT/frontend/public/proof" -CSV="$PROOF_DIR/fsma-204-sample.csv" -OUT="$PROOF_DIR/audit-bundle.zip" - -if [[ ! -f "$CSV" ]]; then - echo "error: $CSV not found" >&2 - exit 1 -fi - -WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT - -cp "$CSV" "$WORK/fsma-204-sample.csv" - -python3 - "$WORK" <<'PY' -import hashlib -import json -import sys -from pathlib import Path - -work = Path(sys.argv[1]) -csv_path = work / "fsma-204-sample.csv" - -def sha256_hex(data: bytes) -> str: - return hashlib.sha256(data).hexdigest() - -def hash_leaf(event_hash: str) -> str: - return hashlib.sha256(f"leaf|{event_hash}".encode("utf-8")).hexdigest() - -def hash_pair(left: str, right: str) -> str: - return hashlib.sha256(f"{left}|{right}".encode("utf-8")).hexdigest() - -def merkle_root(hashes: list[str]) -> str: - if not hashes: - return hashlib.sha256(b"empty").hexdigest() - layer = [hash_leaf(h) for h in hashes] - if len(layer) == 1: - return layer[0] - while len(layer) > 1: - if len(layer) % 2 == 1: - layer.append(layer[-1]) - layer = [hash_pair(layer[i], layer[i + 1]) for i in range(0, len(layer), 2)] - return layer[0] - -csv_bytes = csv_path.read_bytes() -csv_hash = sha256_hex(csv_bytes) - -files = [{"path": "fsma-204-sample.csv", "size": len(csv_bytes), "sha256": csv_hash}] -root = merkle_root([csv_hash]) - -manifest = { - "_comment": "SYNTHETIC — RegEngine /proof audit bundle. Hashes are real; data is not.", - "version": "1.0", - "merkle": { - "algorithm": "sha256", - "leaf_prefix": "leaf|", - "node_separator": "|", - "padding": "duplicate-last", - "leaves": [csv_hash], - "root": root, - }, - "files": files, - "verifier": "frontend/public/sdk/verify_chain.py", -} -(work / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") -print(f"merkle_root={root}") -PY - -(cd "$WORK" && zip -q -X "$OUT" fsma-204-sample.csv manifest.json) -echo "wrote $OUT" diff --git a/frontend/public/proof/audit-bundle-tampered.zip b/services/ingestion/tests/fixtures/proof/audit-bundle-tampered.zip similarity index 100% rename from frontend/public/proof/audit-bundle-tampered.zip rename to services/ingestion/tests/fixtures/proof/audit-bundle-tampered.zip diff --git a/frontend/public/proof/audit-bundle.zip b/services/ingestion/tests/fixtures/proof/audit-bundle.zip similarity index 100% rename from frontend/public/proof/audit-bundle.zip rename to services/ingestion/tests/fixtures/proof/audit-bundle.zip diff --git a/services/ingestion/tests/test_proof_verify_router.py b/services/ingestion/tests/test_proof_verify_router.py index 4b23bb3b6..592569ecf 100644 --- a/services/ingestion/tests/test_proof_verify_router.py +++ b/services/ingestion/tests/test_proof_verify_router.py @@ -576,8 +576,8 @@ def test_unsupported_content_type_rejected(self, client: TestClient): # --------------------------------------------------------------------------- class TestZipRoundTrip: def test_checked_in_public_audit_bundle_verifies(self, client: TestClient): - """The ZIP linked from /proof must verify through the public API.""" - bundle = (REPO_ROOT / "frontend/public/proof/audit-bundle.zip").read_bytes() + """The checked-in fixture ZIP must verify through the public API.""" + bundle = (REPO_ROOT / "services/ingestion/tests/fixtures/proof/audit-bundle.zip").read_bytes() resp = client.post( "/api/v1/proof/verify", @@ -599,7 +599,7 @@ def test_generic_bundle_may_use_unrelated_verification_metadata( tmp_path: Path, ): """Only FDA markers activate persisted-chain status semantics.""" - original = (REPO_ROOT / "frontend/public/proof/audit-bundle.zip").read_bytes() + original = (REPO_ROOT / "services/ingestion/tests/fixtures/proof/audit-bundle.zip").read_bytes() modified = io.BytesIO() with zipfile.ZipFile(io.BytesIO(original), "r") as source: with zipfile.ZipFile(modified, "w", zipfile.ZIP_DEFLATED) as target: @@ -625,8 +625,8 @@ def test_generic_bundle_may_use_unrelated_verification_metadata( assert sdk_verify_bundle(str(bundle_path)) == 0 def test_checked_in_tampered_public_audit_bundle_fails(self, client: TestClient): - """The tampered ZIP linked from /proof must fail verification.""" - bundle = (REPO_ROOT / "frontend/public/proof/audit-bundle-tampered.zip").read_bytes() + """The tampered fixture ZIP must fail verification.""" + bundle = (REPO_ROOT / "services/ingestion/tests/fixtures/proof/audit-bundle-tampered.zip").read_bytes() resp = client.post( "/api/v1/proof/verify",