etch-provenance 0.3.0__tar.gz
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.
- etch_provenance-0.3.0/.dockerignore +16 -0
- etch_provenance-0.3.0/.github/workflows/ci.yml +64 -0
- etch_provenance-0.3.0/.github/workflows/release.yml +67 -0
- etch_provenance-0.3.0/.gitignore +16 -0
- etch_provenance-0.3.0/CHANGELOG.md +179 -0
- etch_provenance-0.3.0/CLAUDE.md +77 -0
- etch_provenance-0.3.0/Dockerfile +25 -0
- etch_provenance-0.3.0/LICENSE.md +76 -0
- etch_provenance-0.3.0/PKG-INFO +357 -0
- etch_provenance-0.3.0/PROJECT_INDEX.md +284 -0
- etch_provenance-0.3.0/README.md +298 -0
- etch_provenance-0.3.0/assent-app/.gitignore +5 -0
- etch_provenance-0.3.0/assent-app/README.md +110 -0
- etch_provenance-0.3.0/assent-app/index.html +22 -0
- etch_provenance-0.3.0/assent-app/package-lock.json +3369 -0
- etch_provenance-0.3.0/assent-app/package.json +33 -0
- etch_provenance-0.3.0/assent-app/postcss.config.js +6 -0
- etch_provenance-0.3.0/assent-app/src/App.tsx +67 -0
- etch_provenance-0.3.0/assent-app/src/components/CheckboxFieldOverlay.tsx +100 -0
- etch_provenance-0.3.0/assent-app/src/components/PdfViewer.tsx +155 -0
- etch_provenance-0.3.0/assent-app/src/components/SignatureField.tsx +119 -0
- etch_provenance-0.3.0/assent-app/src/components/SignaturePad.tsx +129 -0
- etch_provenance-0.3.0/assent-app/src/components/StampFieldOverlay.tsx +159 -0
- etch_provenance-0.3.0/assent-app/src/components/TextFieldOverlay.tsx +193 -0
- etch_provenance-0.3.0/assent-app/src/components/VerifyChain.tsx +243 -0
- etch_provenance-0.3.0/assent-app/src/hooks/useDraggableResizable.ts +194 -0
- etch_provenance-0.3.0/assent-app/src/lib/crypto.ts +131 -0
- etch_provenance-0.3.0/assent-app/src/lib/detectFields.ts +1020 -0
- etch_provenance-0.3.0/assent-app/src/lib/etch.ts +244 -0
- etch_provenance-0.3.0/assent-app/src/lib/handoff.ts +22 -0
- etch_provenance-0.3.0/assent-app/src/lib/hash.ts +70 -0
- etch_provenance-0.3.0/assent-app/src/lib/pdf.ts +460 -0
- etch_provenance-0.3.0/assent-app/src/lib/routing.ts +24 -0
- etch_provenance-0.3.0/assent-app/src/lib/signatures.ts +166 -0
- etch_provenance-0.3.0/assent-app/src/main.tsx +54 -0
- etch_provenance-0.3.0/assent-app/src/routes/Home.tsx +141 -0
- etch_provenance-0.3.0/assent-app/src/routes/Send.tsx +232 -0
- etch_provenance-0.3.0/assent-app/src/routes/Sign.tsx +1320 -0
- etch_provenance-0.3.0/assent-app/src/routes/Verify.tsx +252 -0
- etch_provenance-0.3.0/assent-app/src/styles/index.css +70 -0
- etch_provenance-0.3.0/assent-app/tailwind.config.ts +34 -0
- etch_provenance-0.3.0/assent-app/tsconfig.json +23 -0
- etch_provenance-0.3.0/assent-app/vite.config.ts +40 -0
- etch_provenance-0.3.0/deploy/Caddyfile.assent +77 -0
- etch_provenance-0.3.0/deploy/nginx.conf +44 -0
- etch_provenance-0.3.0/docker-compose.yml +44 -0
- etch_provenance-0.3.0/docs/ETCH_ASSENT_SPEC.md +563 -0
- etch_provenance-0.3.0/docs/RELEASING.md +76 -0
- etch_provenance-0.3.0/docs/dooh-spec.md +254 -0
- etch_provenance-0.3.0/docs/superpowers/plans/2026-07-31-assent-customizable-qr-stamp.md +666 -0
- etch_provenance-0.3.0/docs/superpowers/plans/2026-07-31-assent-resize-multi-signature.md +1359 -0
- etch_provenance-0.3.0/docs/superpowers/specs/2026-07-31-assent-customizable-qr-stamp-design.md +118 -0
- etch_provenance-0.3.0/docs/superpowers/specs/2026-07-31-assent-resize-multi-signature-design.md +203 -0
- etch_provenance-0.3.0/etch/__init__.py +19 -0
- etch_provenance-0.3.0/etch/api.py +497 -0
- etch_provenance-0.3.0/etch/assent_agent.py +606 -0
- etch_provenance-0.3.0/etch/assent_api.py +601 -0
- etch_provenance-0.3.0/etch/assent_docs_api.py +287 -0
- etch_provenance-0.3.0/etch/assent_mcp.py +150 -0
- etch_provenance-0.3.0/etch/auth.py +113 -0
- etch_provenance-0.3.0/etch/backfill_payloads.py +168 -0
- etch_provenance-0.3.0/etch/c2pa.py +463 -0
- etch_provenance-0.3.0/etch/chain.py +267 -0
- etch_provenance-0.3.0/etch/chain_manager.py +94 -0
- etch_provenance-0.3.0/etch/db.py +67 -0
- etch_provenance-0.3.0/etch/dooh/README.md +163 -0
- etch_provenance-0.3.0/etch/dooh/__init__.py +38 -0
- etch_provenance-0.3.0/etch/dooh/canonical.py +17 -0
- etch_provenance-0.3.0/etch/dooh/keys.py +87 -0
- etch_provenance-0.3.0/etch/dooh/manifest.py +45 -0
- etch_provenance-0.3.0/etch/dooh/receipt.py +134 -0
- etch_provenance-0.3.0/etch/dooh/submitter.py +106 -0
- etch_provenance-0.3.0/etch/dooh/verifier.py +138 -0
- etch_provenance-0.3.0/etch/dooh_api.py +320 -0
- etch_provenance-0.3.0/etch/glyph.py +177 -0
- etch_provenance-0.3.0/etch/glyph_api.py +261 -0
- etch_provenance-0.3.0/etch/models.py +146 -0
- etch_provenance-0.3.0/etch/records_api.py +591 -0
- etch_provenance-0.3.0/etch/sdk.py +658 -0
- etch_provenance-0.3.0/etch/server.py +88 -0
- etch_provenance-0.3.0/etch/watermark/__init__.py +33 -0
- etch_provenance-0.3.0/etch/watermark/io.py +111 -0
- etch_provenance-0.3.0/etch/watermark/payload.py +127 -0
- etch_provenance-0.3.0/etch/watermark/pipeline.py +173 -0
- etch_provenance-0.3.0/etch/watermark/spread.py +149 -0
- etch_provenance-0.3.0/etch/watermark/sync.py +93 -0
- etch_provenance-0.3.0/etch/watermark_api.py +220 -0
- etch_provenance-0.3.0/examples/dooh_demo.py +222 -0
- etch_provenance-0.3.0/examples/dooh_reference_player.py +208 -0
- etch_provenance-0.3.0/pyproject.toml +83 -0
- etch_provenance-0.3.0/site/assent/assets/Sign-DHhMBU3l.js +2 -0
- etch_provenance-0.3.0/site/assent/assets/Sign-DHhMBU3l.js.map +1 -0
- etch_provenance-0.3.0/site/assent/assets/index-8FnNW1--.js +11 -0
- etch_provenance-0.3.0/site/assent/assets/index-8FnNW1--.js.map +1 -0
- etch_provenance-0.3.0/site/assent/assets/index-CpI5sL96.css +1 -0
- etch_provenance-0.3.0/site/assent/assets/pdf-eIMbniLM.js +63 -0
- etch_provenance-0.3.0/site/assent/assets/pdf-eIMbniLM.js.map +1 -0
- etch_provenance-0.3.0/site/assent/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- etch_provenance-0.3.0/site/assent/assets/qr-JP79f-a9.js +9 -0
- etch_provenance-0.3.0/site/assent/assets/qr-JP79f-a9.js.map +1 -0
- etch_provenance-0.3.0/site/assent/assets/react--ZppyevY.js +60 -0
- etch_provenance-0.3.0/site/assent/assets/react--ZppyevY.js.map +1 -0
- etch_provenance-0.3.0/site/assent/index.html +24 -0
- etch_provenance-0.3.0/site/index.html +924 -0
- etch_provenance-0.3.0/tests/__init__.py +0 -0
- etch_provenance-0.3.0/tests/test_api.py +121 -0
- etch_provenance-0.3.0/tests/test_assent_agent.py +442 -0
- etch_provenance-0.3.0/tests/test_assent_api.py +506 -0
- etch_provenance-0.3.0/tests/test_assent_docs_api.py +215 -0
- etch_provenance-0.3.0/tests/test_batch_api.py +187 -0
- etch_provenance-0.3.0/tests/test_c2pa.py +353 -0
- etch_provenance-0.3.0/tests/test_chain.py +109 -0
- etch_provenance-0.3.0/tests/test_dooh.py +643 -0
- etch_provenance-0.3.0/tests/test_glyph.py +198 -0
- etch_provenance-0.3.0/tests/test_payload_binding.py +161 -0
- etch_provenance-0.3.0/tests/test_records_api.py +548 -0
- etch_provenance-0.3.0/tests/test_sdk.py +258 -0
- etch_provenance-0.3.0/tests/test_watermark.py +177 -0
- etch_provenance-0.3.0/tests/test_watermark_api.py +198 -0
- etch_provenance-0.3.0/tests/test_watermark_robustness.py +145 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: pip install -e ".[dev]"
|
|
25
|
+
|
|
26
|
+
- name: Lint with ruff
|
|
27
|
+
run: ruff check etch/ tests/
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: pytest
|
|
31
|
+
|
|
32
|
+
assent-frontend:
|
|
33
|
+
# Catches TypeScript regressions + broken build configs for the
|
|
34
|
+
# Etch Assent SPA before they hit main. Runs independently of the
|
|
35
|
+
# Python matrix so a TS issue doesn't block Python CI and vice versa.
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
defaults:
|
|
38
|
+
run:
|
|
39
|
+
working-directory: assent-app
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
|
|
43
|
+
- name: Set up Node
|
|
44
|
+
uses: actions/setup-node@v4
|
|
45
|
+
with:
|
|
46
|
+
node-version: "20"
|
|
47
|
+
cache: "npm"
|
|
48
|
+
cache-dependency-path: assent-app/package-lock.json
|
|
49
|
+
|
|
50
|
+
- name: Install dependencies
|
|
51
|
+
run: npm ci
|
|
52
|
+
|
|
53
|
+
- name: Type-check + build
|
|
54
|
+
# `npm run build` already runs `tsc --noEmit && vite build`
|
|
55
|
+
# per assent-app/package.json.
|
|
56
|
+
run: npm run build
|
|
57
|
+
|
|
58
|
+
- name: Upload built bundle (main only)
|
|
59
|
+
if: github.ref == 'refs/heads/main'
|
|
60
|
+
uses: actions/upload-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: assent-bundle
|
|
63
|
+
path: site/assent/
|
|
64
|
+
retention-days: 14
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # Required for trusted publishing (OIDC)
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: pip install -e ".[dev]"
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: ruff check etch/ tests/
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: pytest
|
|
31
|
+
|
|
32
|
+
build:
|
|
33
|
+
needs: test
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- name: Set up Python
|
|
39
|
+
uses: actions/setup-python@v5
|
|
40
|
+
with:
|
|
41
|
+
python-version: "3.12"
|
|
42
|
+
|
|
43
|
+
- name: Install build tools
|
|
44
|
+
run: pip install build
|
|
45
|
+
|
|
46
|
+
- name: Build sdist and wheel
|
|
47
|
+
run: python -m build
|
|
48
|
+
|
|
49
|
+
- name: Upload build artifacts
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
publish:
|
|
56
|
+
needs: build
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
environment: pypi
|
|
59
|
+
steps:
|
|
60
|
+
- name: Download build artifacts
|
|
61
|
+
uses: actions/download-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist/
|
|
65
|
+
|
|
66
|
+
- name: Publish to PyPI
|
|
67
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Etch are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.0] — 2026-11-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Assent MCP server** (`etch-assent-mcp`, `pip install "etch-provenance[mcp]"`). An agent asks a human
|
|
15
|
+
to sign and gets a link back. `request_signature` renders plain text to a sign-off PDF, or
|
|
16
|
+
takes an existing PDF. It encrypts locally in the browser's format (`[IV || CT]`,
|
|
17
|
+
`#key=…&wt=…`), uploads ciphertext, and stamps `uploaded` at index 0. Etch never holds the
|
|
18
|
+
key. The core is `etch/assent_agent.py`, with no MCP dependency.
|
|
19
|
+
`signature_status` reports progress (sent → opened → signing → finalized) and whether the
|
|
20
|
+
opened copy matches what was sent. `fetch_signed` decrypts the signed return, accepts it only
|
|
21
|
+
when its hash equals the `finalized` event, and recomputes the inclusion proof locally.
|
|
22
|
+
- **Etch Assent** (`/v1/assent/*`) — client-side PDF signing with an anonymous
|
|
23
|
+
provenance chain. The PDF never reaches the server; only hashes and event
|
|
24
|
+
metadata do. Events are pinned to the `assent/public` namespace and capped at
|
|
25
|
+
32 per document; per IP, 30 new documents and 600 follow-on events an hour.
|
|
26
|
+
- `POST /v1/assent/stamp` — commit an `assent.event`
|
|
27
|
+
- `GET /v1/assent/chain/{document_id}` — event chain with integrity check
|
|
28
|
+
- `GET /v1/assent/records/{record_id}` and `…/proof` — public receipt + inclusion proof
|
|
29
|
+
- `GET /v1/assent/verify?hash=` — find events by document hash
|
|
30
|
+
- **Assent send-to-sign (E2EE)** — opaque ciphertext store at
|
|
31
|
+
`/v1/assent/document`. The browser encrypts with AES-256-GCM and keeps the key
|
|
32
|
+
in the URL fragment, which browsers never transmit, so Etch cannot decrypt what
|
|
33
|
+
it holds. 10 uploads per IP per hour, 15 MB cap, disk path via
|
|
34
|
+
`ETCH_ASSENT_DOC_DIR`.
|
|
35
|
+
- **Capability token for document replacement** — `PUT /v1/assent/document/{id}`
|
|
36
|
+
now requires the `X-Assent-Write-Token` issued at upload time, so possession of
|
|
37
|
+
a document ID is no longer sufficient to overwrite it. The sender bakes the
|
|
38
|
+
token into the share link's fragment, so the server never sees it in transit.
|
|
39
|
+
- **Multi-field signing with a review/finalize gate** — several signature fields
|
|
40
|
+
per document, each independently placed, resized, and dragged; a review stage
|
|
41
|
+
between placing and finalizing.
|
|
42
|
+
- **Customizable verification stamp** — the QR verification stamp is draggable,
|
|
43
|
+
resizable, and can be switched off entirely.
|
|
44
|
+
- **DOOH playback receipts** (`/v1/dooh/*`) — bilaterally-signed proof that a
|
|
45
|
+
creative ran on a specific screen at a specific time, anchored in an
|
|
46
|
+
advertiser-owned namespace. Ships with an offline verifier, an ed25519 keypair
|
|
47
|
+
and identity-manifest model, an `EtchSubmitter` client, and a reference player.
|
|
48
|
+
Spec: [`docs/dooh-spec.md`](docs/dooh-spec.md) (CC-BY 4.0).
|
|
49
|
+
- **Glyph** — 8-character Crockford-base32 shortcodes encoding a 32-bit
|
|
50
|
+
`proof_id` plus an 8-bit checksum over the content hash, so typos and
|
|
51
|
+
fabrications are caught before a database lookup. Adds a bar sigil renderer and
|
|
52
|
+
a server-rendered public resolver at `/g/{shortcode}` (plus `.json` and `.png`).
|
|
53
|
+
- **etchmark** — inaudible spread-spectrum audio watermark carrying an Etch
|
|
54
|
+
shortcode, with sync recovery and CRC-12 payload framing.
|
|
55
|
+
`POST /v1/proof/{proof_id}/embed-audio` returns lossless 16-bit PCM WAV;
|
|
56
|
+
`POST /v1/proof/extract-audio` recovers the shortcode and resolves it. WAV and
|
|
57
|
+
FLAC are read natively; MP3/AAC/Opus go through `ffmpeg` when it is on `PATH`.
|
|
58
|
+
The watermark is a *pointer* to a chain record, never a proof in itself.
|
|
59
|
+
- **`if_changed` on `POST /v1/records`** — with `record.id` set, Etch looks up the
|
|
60
|
+
latest record for that `(namespace, external_id, record_type)` and, if the
|
|
61
|
+
`record_hash` is unchanged, returns the existing receipt with
|
|
62
|
+
`"deduplicated": true` without appending. Opt-in on purpose: the chain is
|
|
63
|
+
append-only, and re-registering unchanged content is a legitimate timestamped
|
|
64
|
+
re-attestation, so the default must never silently deduplicate.
|
|
65
|
+
- **`GET /v1/records/stats`** — per-namespace append vs. dedup counters, so the
|
|
66
|
+
saving from `if_changed` is measurable rather than asserted.
|
|
67
|
+
- **Structured logging setup** — uvicorn only configures its own loggers, so
|
|
68
|
+
`etch.*` log lines were being dropped, including the dedup line that is the only
|
|
69
|
+
trace a suppressed write leaves. Level via `ETCH_LOG_LEVEL` (default `INFO`).
|
|
70
|
+
|
|
71
|
+
- **Assent field detection.** Fields are found when a PDF opens, entirely in the
|
|
72
|
+
browser. Fillable PDFs import their own form fields (text, checkbox, radio,
|
|
73
|
+
signature) at the exact widget boxes with existing values; the finalized PDF
|
|
74
|
+
draws the values and strips the original widgets so no viewer paints an empty
|
|
75
|
+
field over them. Flat PDFs get suggested fields from underscore runs, drawn
|
|
76
|
+
underlines, captions under a line, empty table cells (labelled by the cell to
|
|
77
|
+
their left), checkboxes in every form Word produces — Wingdings / Wingdings 2
|
|
78
|
+
symbols, ☐ glyphs, content controls, legacy form checkboxes, drawn squares —
|
|
79
|
+
plus `( )` and `[ ]` blanks, and bare labels like `Name:`. A box the document
|
|
80
|
+
prints as ☑ comes in checked; unchecking it paints the printed mark out.
|
|
81
|
+
Designed forms (InDesign / government templates — light field boxes on a
|
|
82
|
+
tinted panel, question printed at the top of each box) get a field under
|
|
83
|
+
each question; centered column headers, yes/no-only questions, paragraph
|
|
84
|
+
boxes and "office use only" panels are left alone. A blank repeated at the
|
|
85
|
+
same spot on every page (a running "Applicant name" header) is one value.
|
|
86
|
+
An auto-filled date beside a signature line is kept only if that line was
|
|
87
|
+
signed. Date blanks prefill today; name and email blanks follow
|
|
88
|
+
the identity panel. Detected signature boxes are suggestions: they don't block
|
|
89
|
+
finishing and only reach the chain when signed.
|
|
90
|
+
- Assent checkbox fields, placeable by hand as well as detected.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- **Published on PyPI as `etch-provenance`.** `etch` on PyPI is an unrelated project, so
|
|
95
|
+
`pip install etch` never installed this one, and v0.2.0 never reached PyPI. The import
|
|
96
|
+
is still `import etch`.
|
|
97
|
+
- A document can now be finalized on text fields alone; a signature field is no
|
|
98
|
+
longer required.
|
|
99
|
+
- The verification stamp shrank from a full-width banner to a corner badge.
|
|
100
|
+
- A signed Assent signature field can still be moved and resized while placing;
|
|
101
|
+
the finalized PDF contain-fits the ink into the box instead of stretching it.
|
|
102
|
+
- Assent text fields fit their text by default: one line tall, 120pt wide while
|
|
103
|
+
empty, then hugging the typed text as it grows and shrinks (trailing spaces
|
|
104
|
+
trimmed on blur), so a field stops covering the page around it. A manual resize
|
|
105
|
+
pins the box; a **fit** button snaps it back. Fields resize down to one glyph
|
|
106
|
+
by one line, and the finalized text is vertically centered in its box.
|
|
107
|
+
- `POST /v1/proof/batch` accepts up to 1000 items per request.
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
|
|
111
|
+
- **DOOH proofs bind the bundle.** The verifier compared `bundle_hash` to `etch_proof.record_hash`,
|
|
112
|
+
a field the chain leaf doesn't commit to, so a genuine proof for one receipt could be re-pointed at
|
|
113
|
+
another bundle and pass, even against a trusted root. `EtchProof` now carries `payload_json`, and
|
|
114
|
+
`verify_bundle` requires it to hash to `payload_hash` and name the bundle. **Breaking for verifiers:**
|
|
115
|
+
a proof without `payload_json` fails; `GET /v1/dooh/receipts` returns it for anchored bundles.
|
|
116
|
+
- **Send-to-sign verify links resolve.** The link and QR stamped on every send-to-sign PDF carry
|
|
117
|
+
the server-issued document id, which has no `doc_` prefix; the Verify page rejected it as an
|
|
118
|
+
"Unrecognized identifier". Any token that isn't a record id is now looked up as a document id.
|
|
119
|
+
- **Proofs now bind the content hash.** Every chain payload included a `registered_at` that was
|
|
120
|
+
never stored, so a verifier could recompute `leaf_hash` and `mmr_root` but couldn't connect
|
|
121
|
+
`payload_hash` to the content: that link rested on Etch's database. The exact hashed payload
|
|
122
|
+
is now stored (`payload_json` on `etch_records` and `etch_proofs`, added at startup) and
|
|
123
|
+
returned by the `/v1/records`, `/v1/assent` and `/v1/c2pa` proofs. The verification steps go
|
|
124
|
+
from 2 to 4, and the SDK's `InclusionProof` gains `payload_json` and `verify()`. Older rows were
|
|
125
|
+
recovered with `python -m etch.backfill_payloads`, which brute-forces `registered_at` over the
|
|
126
|
+
doubles just below the stored chain timestamp. Production: 137,663/137,663 recovered
|
|
127
|
+
(2026-11-03), each one an exact SHA-256 preimage.
|
|
128
|
+
|
|
129
|
+
- Assent's rate limit counted every event, so one long form with several
|
|
130
|
+
signature lines used most of an office NAT's hourly budget. The per-IP limit
|
|
131
|
+
is now on new documents (30/hour), with a 600/hour flood guard on follow-on
|
|
132
|
+
events. A refused `created` event no longer leaves the session dead: the
|
|
133
|
+
client retries it before the next write, and a 429 explains itself instead
|
|
134
|
+
of surfacing the raw API detail.
|
|
135
|
+
- The drawn-signature pad opens as a modal instead of at the bottom of the
|
|
136
|
+
sidebar, where it was off-screen when signing deep in a long document.
|
|
137
|
+
- Drawn signatures are stamped with a transparent background, so the ink no
|
|
138
|
+
longer erases the signature line it's placed on.
|
|
139
|
+
- Index `idx_records_ns_ext` on `(namespace_id, external_id, record_type)` keeps
|
|
140
|
+
the `if_changed` lookup flat as external IDs accumulate, instead of degrading
|
|
141
|
+
into a scan.
|
|
142
|
+
- Assent field placement is predictable, and a placed signature no longer hides
|
|
143
|
+
text beneath it.
|
|
144
|
+
- Assent "remove field" and "remove stamp" work — pressing them no longer starts
|
|
145
|
+
a drag on the field, which had swallowed the click and opened the signature pad.
|
|
146
|
+
|
|
147
|
+
### Removed
|
|
148
|
+
|
|
149
|
+
- Internal outreach documents and infrastructure details are no longer committed
|
|
150
|
+
to the public repository (`docs/_private/` is now gitignored).
|
|
151
|
+
|
|
152
|
+
## [0.2.0] — 2026-03-27
|
|
153
|
+
|
|
154
|
+
### Added
|
|
155
|
+
|
|
156
|
+
- **System of Record API** (`/v1/records/*`) — namespace-isolated chains with API
|
|
157
|
+
key authentication. Keys are formatted `etch_{live|test}_sk_{token}` and stored
|
|
158
|
+
only as SHA-256 hashes. Includes cursor pagination, self-contained inclusion
|
|
159
|
+
proofs, and `GET /v1/chain/root`.
|
|
160
|
+
- **C2PA compatibility layer** (`/v1/c2pa/*`) — register, retrieve, verify, and
|
|
161
|
+
bridge C2PA manifests against the Etch chain, for EU AI Act provenance work.
|
|
162
|
+
- **Batch registration** — `POST /v1/proof/batch`.
|
|
163
|
+
- **Landing site** (`site/index.html`) and deployment infrastructure: Dockerfile,
|
|
164
|
+
`docker-compose.yml`, and reverse-proxy configuration.
|
|
165
|
+
|
|
166
|
+
## [0.1.0] — 2026-03-18
|
|
167
|
+
|
|
168
|
+
### Added
|
|
169
|
+
|
|
170
|
+
- Initial extraction of Etch as a standalone project: the MMR audit chain
|
|
171
|
+
(`AuditChain`, `ChainEntry`, `InclusionProof`), offline verification via
|
|
172
|
+
`verify_inclusion_proof()`, and the legacy `/v1/proof/*` registration API.
|
|
173
|
+
- Async Python SDK (`EtchClient`).
|
|
174
|
+
- CI matrix across Python 3.11/3.12/3.13 with ruff linting, and a release
|
|
175
|
+
workflow publishing to PyPI via OIDC trusted publishing.
|
|
176
|
+
|
|
177
|
+
[Unreleased]: https://github.com/maco144/Etch/compare/v0.3.0...HEAD
|
|
178
|
+
[0.3.0]: https://github.com/maco144/Etch/releases/tag/v0.3.0
|
|
179
|
+
[0.2.0]: https://github.com/maco144/Etch/releases/tag/v0.2.0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# CLAUDE.md — Etch
|
|
2
|
+
|
|
3
|
+
## What is Etch?
|
|
4
|
+
|
|
5
|
+
Content provenance on a tamper-evident Merkle chain. Register, verify, and prove content existed at a point in time. Privacy-preserving (content never stored, only SHA-256 hashes).
|
|
6
|
+
|
|
7
|
+
## Quick Reference
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Setup
|
|
11
|
+
python -m venv .venv && source .venv/bin/activate
|
|
12
|
+
pip install -e ".[dev]"
|
|
13
|
+
|
|
14
|
+
# Run tests
|
|
15
|
+
pytest
|
|
16
|
+
|
|
17
|
+
# Run server (default port 8100)
|
|
18
|
+
uvicorn etch.server:app --reload
|
|
19
|
+
|
|
20
|
+
# Lint
|
|
21
|
+
ruff check etch/ tests/
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
- **chain.py** — Core: MMR hash chain (`AuditChain`), inclusion proofs, offline verification
|
|
27
|
+
- **chain_manager.py** — Namespace-isolated chain manager (per-tenant chains for SoR API)
|
|
28
|
+
- **api.py** — FastAPI router at `/v1/proof/*` (legacy: register, verify, lookup, batch, stats)
|
|
29
|
+
- **records_api.py** — FastAPI router at `/v1/records/*` (SoR API, namespace-isolated, API key auth)
|
|
30
|
+
- **c2pa.py** — FastAPI router at `/v1/c2pa/*` (C2PA manifest bridge for EU AI Act compliance)
|
|
31
|
+
- **assent_api.py** — FastAPI router at `/v1/assent/*` (anonymous PDF-signer event chain)
|
|
32
|
+
- **assent_docs_api.py** — FastAPI router at `/v1/assent/document/*` (E2EE ciphertext store)
|
|
33
|
+
- **assent_agent.py / assent_mcp.py** — Assent sender for agents (text→PDF, local E2EE, sign link) + stdio MCP server `etch-assent-mcp`
|
|
34
|
+
- **dooh_api.py** — FastAPI router at `/v1/dooh/*` (playback receipts); SDK in **dooh/**
|
|
35
|
+
- **glyph.py / glyph_api.py** — Shortcodes + bar sigils; public resolver at `/g/{shortcode}`
|
|
36
|
+
- **watermark/ + watermark_api.py** — `etchmark` audio watermark; embed/extract endpoints
|
|
37
|
+
- **auth.py** — API key authentication (`etch_{mode}_sk_{token}`), namespace bootstrap
|
|
38
|
+
- **models.py** — SQLAlchemy ORM: `ProofRecord`, `Namespace`, `ApiKey`, `RecordEntry` (4 tables)
|
|
39
|
+
- **db.py** — Async DB sessions (SQLite default, PostgreSQL via `ETCH_DATABASE_URL`)
|
|
40
|
+
- **sdk.py** — Async Python SDK (`EtchClient`) with legacy + v2 API support
|
|
41
|
+
- **backfill_payloads.py** — One-off recovery of `payload_json` for rows written before it was stored
|
|
42
|
+
- **server.py** — FastAPI app with lifespan (auto-creates tables, bootstraps namespaces, configures `etch` loggers)
|
|
43
|
+
|
|
44
|
+
## Key Patterns
|
|
45
|
+
|
|
46
|
+
- Global singleton chain via `get_chain()` / `log_event()` in chain.py
|
|
47
|
+
- Thread-safe chain operations (threading.Lock)
|
|
48
|
+
- Persist hook pattern: chain calls sync hook after append (outside lock)
|
|
49
|
+
- **Every chain write must persist `entry.payload_json`** (the `payload_json` column). It is the only link from `payload_hash` to the content hash; a router that drops it ships proofs that can't be tied to content. Proof endpoints return it alongside `chain.RECORD_COMMIT_VERIFICATION_STEPS`
|
|
50
|
+
- New columns on existing tables go in `db._ADDED_COLUMNS` (nullable); `create_all` never alters a deployed table
|
|
51
|
+
- API tests use httpx ASGITransport with mocked DB layer
|
|
52
|
+
- `asyncio_mode = "auto"` in pytest config
|
|
53
|
+
- The chain is append-only and every root hashes the previous one — nothing is ever deleted or compacted, so write-amplification is permanent. A write-only API invites clients to re-register their whole working set every run; `POST /v1/records` takes `if_changed: true` (with `record.id`) to make that cheap: one indexed lookup on `idx_records_ns_ext`, and an unchanged `record_hash` returns the existing receipt with `deduplicated: true` instead of appending. It is opt-in on purpose — re-registering unchanged content is a legitimate timestamped re-attestation, so the default must never silently deduplicate.
|
|
54
|
+
|
|
55
|
+
## Environment Variables
|
|
56
|
+
|
|
57
|
+
- `ETCH_DATABASE_URL` or `DATABASE_URL` — DB connection string (default: `sqlite+aiosqlite:///./etch.db`)
|
|
58
|
+
- `ETCH_LOG_LEVEL` — Level for the `etch` loggers (default: `INFO`)
|
|
59
|
+
- `ETCH_ASSENT_DOC_DIR` — Assent ciphertext storage (default: `/var/etch/assent-documents`)
|
|
60
|
+
- `ETCH_ASSENT_IP_SALT` — Salt for hashed client IPs in Assent rate limiting
|
|
61
|
+
|
|
62
|
+
## Documentation Map
|
|
63
|
+
|
|
64
|
+
- **README.md** — public front door: full endpoint tables, auth, configuration
|
|
65
|
+
- **CHANGELOG.md** — Keep a Changelog; `v0.2.0` is tagged, everything since is Unreleased
|
|
66
|
+
- **PROJECT_INDEX.md** — repo map, module-by-module; read before exploring
|
|
67
|
+
- **docs/ETCH_ASSENT_SPEC.md** — Assent; sections marked *as built* are authoritative,
|
|
68
|
+
*planned* ones may not exist yet
|
|
69
|
+
- **docs/dooh-spec.md** — DOOH protocol; the trust model section is non-negotiable context
|
|
70
|
+
- **etch/dooh/README.md** — DOOH SDK quickstart
|
|
71
|
+
- **assent-app/README.md** — Assent frontend build + architecture
|
|
72
|
+
- **docs/RELEASING.md** — release procedure (PyPI via OIDC)
|
|
73
|
+
|
|
74
|
+
## Conventions
|
|
75
|
+
|
|
76
|
+
- Python 3.11+, ruff line-length 120
|
|
77
|
+
- License: Rising Sun License v1.0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FROM python:3.12-slim AS base
|
|
2
|
+
|
|
3
|
+
RUN groupadd -r etch && useradd -r -g etch -d /app etch
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
COPY pyproject.toml README.md LICENSE.md ./
|
|
7
|
+
COPY etch/ etch/
|
|
8
|
+
RUN pip install --no-cache-dir ".[postgres]"
|
|
9
|
+
|
|
10
|
+
COPY site/ site/
|
|
11
|
+
|
|
12
|
+
# Pre-create the assent document store with correct ownership. Docker will
|
|
13
|
+
# adopt this as the initial state of the named volume on first boot.
|
|
14
|
+
RUN mkdir -p /data/assent-documents && chown -R etch:etch /data
|
|
15
|
+
|
|
16
|
+
USER etch
|
|
17
|
+
EXPOSE 8100
|
|
18
|
+
|
|
19
|
+
# --workers 1 is intentional. The anonymous assent endpoints use an in-memory
|
|
20
|
+
# sliding-window rate limiter that lives per-process; multiple workers would
|
|
21
|
+
# multiply the effective cap. When we move to a shared store (Redis), scale up.
|
|
22
|
+
# --forwarded-allow-ips=* lets uvicorn honour Caddy's X-Forwarded-Proto so the glyph
|
|
23
|
+
# resolver mints https:// magic URLs. Safe: the port is published on 127.0.0.1 only,
|
|
24
|
+
# so the only client that can set those headers is the local reverse proxy.
|
|
25
|
+
CMD ["uvicorn", "etch.server:app", "--host", "0.0.0.0", "--port", "8100", "--workers", "1", "--forwarded-allow-ips", "*"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Rising Sun License v1.0
|
|
2
|
+
|
|
3
|
+
This license governs all software released under the Rising Sun project.
|
|
4
|
+
|
|
5
|
+
## For People
|
|
6
|
+
|
|
7
|
+
Use this software however you want. Run it, study it, modify it, share it.
|
|
8
|
+
Personal use, education, research, tinkering — all free, no strings attached.
|
|
9
|
+
|
|
10
|
+
You don't need permission. You don't need to register. Just use it.
|
|
11
|
+
|
|
12
|
+
## For Builders
|
|
13
|
+
|
|
14
|
+
If you build something that generates revenue using this software, connect it
|
|
15
|
+
to the Nous network. Nous handles verification, attestation, and accounting
|
|
16
|
+
automatically — you integrate once and it works.
|
|
17
|
+
|
|
18
|
+
The economics are simple: Nous takes a small percentage of the value flowing
|
|
19
|
+
through your deployment. The rate is published and predictable. You always
|
|
20
|
+
keep the majority. The exact terms are governed by the Nous protocol and
|
|
21
|
+
published at the canonical terms endpoint.
|
|
22
|
+
|
|
23
|
+
You may not remove, bypass, or substitute the Nous integration in a
|
|
24
|
+
commercial deployment. Everything else is yours to build.
|
|
25
|
+
|
|
26
|
+
## For Enterprise
|
|
27
|
+
|
|
28
|
+
Custom integration, dedicated support, and private deployment are available
|
|
29
|
+
directly from Rising Sun. Enterprise terms are negotiated individually and
|
|
30
|
+
may include modified Nous configurations, SLAs, and priority access to new
|
|
31
|
+
capabilities.
|
|
32
|
+
|
|
33
|
+
Contact: alex@risingsun.name
|
|
34
|
+
|
|
35
|
+
## How Nous Works
|
|
36
|
+
|
|
37
|
+
Nous is a verification and attestation service. When your deployment does
|
|
38
|
+
work — processes data, serves users, executes transactions — Nous verifies
|
|
39
|
+
the work, attests to its completion, and routes value through the Ergon
|
|
40
|
+
ledger. Trust scores build over time: consistent, honest operation earns
|
|
41
|
+
better terms automatically.
|
|
42
|
+
|
|
43
|
+
This is not a phone-home or surveillance mechanism. Nous verifies value
|
|
44
|
+
created, not behavior observed. The attestation record is transparent
|
|
45
|
+
and auditable.
|
|
46
|
+
|
|
47
|
+
## What You Can Do
|
|
48
|
+
|
|
49
|
+
- Use, copy, modify, and distribute this software
|
|
50
|
+
- Build commercial products and services on it
|
|
51
|
+
- Fork it, extend it, combine it with other software
|
|
52
|
+
- Run it on any infrastructure you control
|
|
53
|
+
- Contribute improvements back (appreciated, not required)
|
|
54
|
+
|
|
55
|
+
## What You Cannot Do
|
|
56
|
+
|
|
57
|
+
- Remove or bypass Nous integration in commercial deployments
|
|
58
|
+
- Represent modified versions as official Rising Sun releases
|
|
59
|
+
- Use Rising Sun trademarks without permission
|
|
60
|
+
|
|
61
|
+
## Contributions
|
|
62
|
+
|
|
63
|
+
Contributions via pull request grant the project a perpetual, irrevocable,
|
|
64
|
+
royalty-free license to use the contribution. Contributors retain copyright
|
|
65
|
+
on their work.
|
|
66
|
+
|
|
67
|
+
## No Warranty
|
|
68
|
+
|
|
69
|
+
This software is provided as-is, without warranty of any kind. The licensor
|
|
70
|
+
is not liable for any damages arising from its use.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
Rising Sun License v1.0
|
|
75
|
+
Copyright (c) 2026 Alex Macaluso
|
|
76
|
+
https://risingsun.name
|