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.
Files changed (120) hide show
  1. etch_provenance-0.3.0/.dockerignore +16 -0
  2. etch_provenance-0.3.0/.github/workflows/ci.yml +64 -0
  3. etch_provenance-0.3.0/.github/workflows/release.yml +67 -0
  4. etch_provenance-0.3.0/.gitignore +16 -0
  5. etch_provenance-0.3.0/CHANGELOG.md +179 -0
  6. etch_provenance-0.3.0/CLAUDE.md +77 -0
  7. etch_provenance-0.3.0/Dockerfile +25 -0
  8. etch_provenance-0.3.0/LICENSE.md +76 -0
  9. etch_provenance-0.3.0/PKG-INFO +357 -0
  10. etch_provenance-0.3.0/PROJECT_INDEX.md +284 -0
  11. etch_provenance-0.3.0/README.md +298 -0
  12. etch_provenance-0.3.0/assent-app/.gitignore +5 -0
  13. etch_provenance-0.3.0/assent-app/README.md +110 -0
  14. etch_provenance-0.3.0/assent-app/index.html +22 -0
  15. etch_provenance-0.3.0/assent-app/package-lock.json +3369 -0
  16. etch_provenance-0.3.0/assent-app/package.json +33 -0
  17. etch_provenance-0.3.0/assent-app/postcss.config.js +6 -0
  18. etch_provenance-0.3.0/assent-app/src/App.tsx +67 -0
  19. etch_provenance-0.3.0/assent-app/src/components/CheckboxFieldOverlay.tsx +100 -0
  20. etch_provenance-0.3.0/assent-app/src/components/PdfViewer.tsx +155 -0
  21. etch_provenance-0.3.0/assent-app/src/components/SignatureField.tsx +119 -0
  22. etch_provenance-0.3.0/assent-app/src/components/SignaturePad.tsx +129 -0
  23. etch_provenance-0.3.0/assent-app/src/components/StampFieldOverlay.tsx +159 -0
  24. etch_provenance-0.3.0/assent-app/src/components/TextFieldOverlay.tsx +193 -0
  25. etch_provenance-0.3.0/assent-app/src/components/VerifyChain.tsx +243 -0
  26. etch_provenance-0.3.0/assent-app/src/hooks/useDraggableResizable.ts +194 -0
  27. etch_provenance-0.3.0/assent-app/src/lib/crypto.ts +131 -0
  28. etch_provenance-0.3.0/assent-app/src/lib/detectFields.ts +1020 -0
  29. etch_provenance-0.3.0/assent-app/src/lib/etch.ts +244 -0
  30. etch_provenance-0.3.0/assent-app/src/lib/handoff.ts +22 -0
  31. etch_provenance-0.3.0/assent-app/src/lib/hash.ts +70 -0
  32. etch_provenance-0.3.0/assent-app/src/lib/pdf.ts +460 -0
  33. etch_provenance-0.3.0/assent-app/src/lib/routing.ts +24 -0
  34. etch_provenance-0.3.0/assent-app/src/lib/signatures.ts +166 -0
  35. etch_provenance-0.3.0/assent-app/src/main.tsx +54 -0
  36. etch_provenance-0.3.0/assent-app/src/routes/Home.tsx +141 -0
  37. etch_provenance-0.3.0/assent-app/src/routes/Send.tsx +232 -0
  38. etch_provenance-0.3.0/assent-app/src/routes/Sign.tsx +1320 -0
  39. etch_provenance-0.3.0/assent-app/src/routes/Verify.tsx +252 -0
  40. etch_provenance-0.3.0/assent-app/src/styles/index.css +70 -0
  41. etch_provenance-0.3.0/assent-app/tailwind.config.ts +34 -0
  42. etch_provenance-0.3.0/assent-app/tsconfig.json +23 -0
  43. etch_provenance-0.3.0/assent-app/vite.config.ts +40 -0
  44. etch_provenance-0.3.0/deploy/Caddyfile.assent +77 -0
  45. etch_provenance-0.3.0/deploy/nginx.conf +44 -0
  46. etch_provenance-0.3.0/docker-compose.yml +44 -0
  47. etch_provenance-0.3.0/docs/ETCH_ASSENT_SPEC.md +563 -0
  48. etch_provenance-0.3.0/docs/RELEASING.md +76 -0
  49. etch_provenance-0.3.0/docs/dooh-spec.md +254 -0
  50. etch_provenance-0.3.0/docs/superpowers/plans/2026-07-31-assent-customizable-qr-stamp.md +666 -0
  51. etch_provenance-0.3.0/docs/superpowers/plans/2026-07-31-assent-resize-multi-signature.md +1359 -0
  52. etch_provenance-0.3.0/docs/superpowers/specs/2026-07-31-assent-customizable-qr-stamp-design.md +118 -0
  53. etch_provenance-0.3.0/docs/superpowers/specs/2026-07-31-assent-resize-multi-signature-design.md +203 -0
  54. etch_provenance-0.3.0/etch/__init__.py +19 -0
  55. etch_provenance-0.3.0/etch/api.py +497 -0
  56. etch_provenance-0.3.0/etch/assent_agent.py +606 -0
  57. etch_provenance-0.3.0/etch/assent_api.py +601 -0
  58. etch_provenance-0.3.0/etch/assent_docs_api.py +287 -0
  59. etch_provenance-0.3.0/etch/assent_mcp.py +150 -0
  60. etch_provenance-0.3.0/etch/auth.py +113 -0
  61. etch_provenance-0.3.0/etch/backfill_payloads.py +168 -0
  62. etch_provenance-0.3.0/etch/c2pa.py +463 -0
  63. etch_provenance-0.3.0/etch/chain.py +267 -0
  64. etch_provenance-0.3.0/etch/chain_manager.py +94 -0
  65. etch_provenance-0.3.0/etch/db.py +67 -0
  66. etch_provenance-0.3.0/etch/dooh/README.md +163 -0
  67. etch_provenance-0.3.0/etch/dooh/__init__.py +38 -0
  68. etch_provenance-0.3.0/etch/dooh/canonical.py +17 -0
  69. etch_provenance-0.3.0/etch/dooh/keys.py +87 -0
  70. etch_provenance-0.3.0/etch/dooh/manifest.py +45 -0
  71. etch_provenance-0.3.0/etch/dooh/receipt.py +134 -0
  72. etch_provenance-0.3.0/etch/dooh/submitter.py +106 -0
  73. etch_provenance-0.3.0/etch/dooh/verifier.py +138 -0
  74. etch_provenance-0.3.0/etch/dooh_api.py +320 -0
  75. etch_provenance-0.3.0/etch/glyph.py +177 -0
  76. etch_provenance-0.3.0/etch/glyph_api.py +261 -0
  77. etch_provenance-0.3.0/etch/models.py +146 -0
  78. etch_provenance-0.3.0/etch/records_api.py +591 -0
  79. etch_provenance-0.3.0/etch/sdk.py +658 -0
  80. etch_provenance-0.3.0/etch/server.py +88 -0
  81. etch_provenance-0.3.0/etch/watermark/__init__.py +33 -0
  82. etch_provenance-0.3.0/etch/watermark/io.py +111 -0
  83. etch_provenance-0.3.0/etch/watermark/payload.py +127 -0
  84. etch_provenance-0.3.0/etch/watermark/pipeline.py +173 -0
  85. etch_provenance-0.3.0/etch/watermark/spread.py +149 -0
  86. etch_provenance-0.3.0/etch/watermark/sync.py +93 -0
  87. etch_provenance-0.3.0/etch/watermark_api.py +220 -0
  88. etch_provenance-0.3.0/examples/dooh_demo.py +222 -0
  89. etch_provenance-0.3.0/examples/dooh_reference_player.py +208 -0
  90. etch_provenance-0.3.0/pyproject.toml +83 -0
  91. etch_provenance-0.3.0/site/assent/assets/Sign-DHhMBU3l.js +2 -0
  92. etch_provenance-0.3.0/site/assent/assets/Sign-DHhMBU3l.js.map +1 -0
  93. etch_provenance-0.3.0/site/assent/assets/index-8FnNW1--.js +11 -0
  94. etch_provenance-0.3.0/site/assent/assets/index-8FnNW1--.js.map +1 -0
  95. etch_provenance-0.3.0/site/assent/assets/index-CpI5sL96.css +1 -0
  96. etch_provenance-0.3.0/site/assent/assets/pdf-eIMbniLM.js +63 -0
  97. etch_provenance-0.3.0/site/assent/assets/pdf-eIMbniLM.js.map +1 -0
  98. etch_provenance-0.3.0/site/assent/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  99. etch_provenance-0.3.0/site/assent/assets/qr-JP79f-a9.js +9 -0
  100. etch_provenance-0.3.0/site/assent/assets/qr-JP79f-a9.js.map +1 -0
  101. etch_provenance-0.3.0/site/assent/assets/react--ZppyevY.js +60 -0
  102. etch_provenance-0.3.0/site/assent/assets/react--ZppyevY.js.map +1 -0
  103. etch_provenance-0.3.0/site/assent/index.html +24 -0
  104. etch_provenance-0.3.0/site/index.html +924 -0
  105. etch_provenance-0.3.0/tests/__init__.py +0 -0
  106. etch_provenance-0.3.0/tests/test_api.py +121 -0
  107. etch_provenance-0.3.0/tests/test_assent_agent.py +442 -0
  108. etch_provenance-0.3.0/tests/test_assent_api.py +506 -0
  109. etch_provenance-0.3.0/tests/test_assent_docs_api.py +215 -0
  110. etch_provenance-0.3.0/tests/test_batch_api.py +187 -0
  111. etch_provenance-0.3.0/tests/test_c2pa.py +353 -0
  112. etch_provenance-0.3.0/tests/test_chain.py +109 -0
  113. etch_provenance-0.3.0/tests/test_dooh.py +643 -0
  114. etch_provenance-0.3.0/tests/test_glyph.py +198 -0
  115. etch_provenance-0.3.0/tests/test_payload_binding.py +161 -0
  116. etch_provenance-0.3.0/tests/test_records_api.py +548 -0
  117. etch_provenance-0.3.0/tests/test_sdk.py +258 -0
  118. etch_provenance-0.3.0/tests/test_watermark.py +177 -0
  119. etch_provenance-0.3.0/tests/test_watermark_api.py +198 -0
  120. etch_provenance-0.3.0/tests/test_watermark_robustness.py +145 -0
@@ -0,0 +1,16 @@
1
+ .git
2
+ .github
3
+ .venv
4
+ .env
5
+ .pytest_cache
6
+ .mypy_cache
7
+ .ruff_cache
8
+ __pycache__
9
+ *.pyc
10
+ *.pyo
11
+ etch.db
12
+ *.egg-info
13
+ dist
14
+ build
15
+ docs
16
+ tests
@@ -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,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.db
8
+ .env
9
+ .venv/
10
+ venv/
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+
15
+ # Internal/private docs — never commit
16
+ docs/_private/
@@ -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