@jterrazz/attestation 0.1.0

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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @jterrazz/attestation
2
+
3
+ Cryptographic attestation for articles — an EIP-712 signature over the canonicalized content, anchored to Bitcoin via OpenTimestamps. Prove an article existed, unmodified, at a point in time, signed by its author.
4
+
5
+ ## Entries
6
+
7
+ | Import | Runs in | Carries |
8
+ | ------------------------------- | ----------- | -------------------------------------------------------------------------------- |
9
+ | `@jterrazz/attestation` | Node | full surface: canonicalize, EIP-712 schema, create/sign/verify, serialize, audit |
10
+ | `@jterrazz/attestation/browser` | any runtime | verify-only, pure ESM (noble-hashes + viem): `verifyFromUrl`, ENS helpers |
11
+ | `@jterrazz/attestation/node` | Node | OpenTimestamps: `stampDigest`, `upgradeProof`, `verifyOts` |
12
+ | `npx attestation` (CLI) | Node | `sign`, `verify`, `upgrade` |
13
+
14
+ ## Flow
15
+
16
+ 1. **Sign** — canonicalize the article body, digest it (SHA-256), build the EIP-712 `AttestationMessage`, sign with the author key → `<article>.attestation.json`.
17
+ 2. **Stamp** — `stampDigest` submits the digest to OpenTimestamps calendars → `<article>.ots`.
18
+ 3. **Upgrade** — once anchored in a Bitcoin block, `upgradeProof` completes the proof.
19
+ 4. **Verify** — anywhere: signature (`verifyAttestation`), timestamp (`verifyOts`), or both from a URL in the browser (`verifyFromUrl`).
20
+
21
+ ## Tests
22
+
23
+ `npm test` (offline, deterministic). `npm run test:network` opts into the OpenTimestamps-calendar e2e suite.
24
+
25
+ Reference consumer: [`jterrazz-web`](https://github.com/jterrazz/jterrazz-web) (verify page, proof card, signing scripts).
26
+
27
+ MIT © [Jean-Baptiste Terrazzoni](https://github.com/jterrazz)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js');
package/dist/audit.cjs ADDED
@@ -0,0 +1,57 @@
1
+ //#region src/core/audit.ts
2
+ /**
3
+ * Suspicious-character audit — runs at SIGN time, never at verify time.
4
+ *
5
+ * Verify must always remain a function of canonicalize() alone, never adding
6
+ * stricter rules. Audit is a separate, mutable layer of advice for the author:
7
+ * "you probably don't want these invisible chars in a published article".
8
+ */
9
+ const SUSPICIOUS = /* @__PURE__ */ new Map([
10
+ [8203, "zero-width space"],
11
+ [8204, "zero-width non-joiner"],
12
+ [8205, "zero-width joiner"],
13
+ [8288, "word joiner"],
14
+ [65279, "zero-width no-break space (interior BOM)"],
15
+ [8234, "left-to-right embedding"],
16
+ [8235, "right-to-left embedding"],
17
+ [8236, "pop directional formatting"],
18
+ [8237, "left-to-right override"],
19
+ [8238, "right-to-left override"],
20
+ [8294, "left-to-right isolate"],
21
+ [8295, "right-to-left isolate"],
22
+ [8296, "first strong isolate"],
23
+ [8297, "pop directional isolate"]
24
+ ]);
25
+ function audit(canonical) {
26
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(canonical);
27
+ const findings = [];
28
+ let line = 1;
29
+ let column = 1;
30
+ for (const ch of text) {
31
+ const cp = ch.codePointAt(0);
32
+ if (cp === void 0) continue;
33
+ if (cp === 10) {
34
+ line++;
35
+ column = 1;
36
+ continue;
37
+ }
38
+ const name = SUSPICIOUS.get(cp);
39
+ if (name !== void 0) findings.push({
40
+ codepoint: cp,
41
+ column,
42
+ line,
43
+ name
44
+ });
45
+ column++;
46
+ }
47
+ return findings;
48
+ }
49
+ //#endregion
50
+ Object.defineProperty(exports, "audit", {
51
+ enumerable: true,
52
+ get: function() {
53
+ return audit;
54
+ }
55
+ });
56
+
57
+ //# sourceMappingURL=audit.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.cjs","names":[],"sources":["../src/core/audit.ts"],"sourcesContent":["/**\n * Suspicious-character audit — runs at SIGN time, never at verify time.\n *\n * Verify must always remain a function of canonicalize() alone, never adding\n * stricter rules. Audit is a separate, mutable layer of advice for the author:\n * \"you probably don't want these invisible chars in a published article\".\n */\n\nconst SUSPICIOUS: ReadonlyMap<number, string> = new Map([\n [0x20_0b, 'zero-width space'],\n [0x200c, 'zero-width non-joiner'],\n [0x200d, 'zero-width joiner'],\n [0x20_60, 'word joiner'],\n [0xfeff, 'zero-width no-break space (interior BOM)'],\n [0x20_2a, 'left-to-right embedding'],\n [0x202b, 'right-to-left embedding'],\n [0x202c, 'pop directional formatting'],\n [0x202d, 'left-to-right override'],\n [0x202e, 'right-to-left override'],\n [0x20_66, 'left-to-right isolate'],\n [0x20_67, 'right-to-left isolate'],\n [0x20_68, 'first strong isolate'],\n [0x20_69, 'pop directional isolate'],\n]);\n\nexport type AuditFinding = {\n line: number;\n column: number;\n codepoint: number;\n name: string;\n};\n\nexport function audit(canonical: Uint8Array): AuditFinding[] {\n const text = new TextDecoder('utf-8', { fatal: true }).decode(canonical);\n const findings: AuditFinding[] = [];\n let line = 1;\n let column = 1;\n\n for (const ch of text) {\n const cp = ch.codePointAt(0);\n if (cp === undefined) {\n continue;\n }\n if (cp === 0x0a) {\n line++;\n column = 1;\n continue;\n }\n const name = SUSPICIOUS.get(cp);\n if (name !== undefined) {\n findings.push({ codepoint: cp, column, line, name });\n }\n column++;\n }\n\n return findings;\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,6BAA0C,IAAI,IAAI;CACpD,CAAC,MAAS,kBAAkB;CAC5B,CAAC,MAAQ,uBAAuB;CAChC,CAAC,MAAQ,mBAAmB;CAC5B,CAAC,MAAS,aAAa;CACvB,CAAC,OAAQ,0CAA0C;CACnD,CAAC,MAAS,yBAAyB;CACnC,CAAC,MAAQ,yBAAyB;CAClC,CAAC,MAAQ,4BAA4B;CACrC,CAAC,MAAQ,wBAAwB;CACjC,CAAC,MAAQ,wBAAwB;CACjC,CAAC,MAAS,uBAAuB;CACjC,CAAC,MAAS,uBAAuB;CACjC,CAAC,MAAS,sBAAsB;CAChC,CAAC,MAAS,yBAAyB;AACvC,CAAC;AASD,SAAgB,MAAM,WAAuC;CACzD,MAAM,OAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,SAAS;CACvE,MAAM,WAA2B,CAAC;CAClC,IAAI,OAAO;CACX,IAAI,SAAS;CAEb,KAAK,MAAM,MAAM,MAAM;EACnB,MAAM,KAAK,GAAG,YAAY,CAAC;EAC3B,IAAI,OAAO,KAAA,GACP;EAEJ,IAAI,OAAO,IAAM;GACb;GACA,SAAS;GACT;EACJ;EACA,MAAM,OAAO,WAAW,IAAI,EAAE;EAC9B,IAAI,SAAS,KAAA,GACT,SAAS,KAAK;GAAE,WAAW;GAAI;GAAQ;GAAM;EAAK,CAAC;EAEvD;CACJ;CAEA,OAAO;AACX"}
package/dist/audit.js ADDED
@@ -0,0 +1,52 @@
1
+ //#region src/core/audit.ts
2
+ /**
3
+ * Suspicious-character audit — runs at SIGN time, never at verify time.
4
+ *
5
+ * Verify must always remain a function of canonicalize() alone, never adding
6
+ * stricter rules. Audit is a separate, mutable layer of advice for the author:
7
+ * "you probably don't want these invisible chars in a published article".
8
+ */
9
+ const SUSPICIOUS = /* @__PURE__ */ new Map([
10
+ [8203, "zero-width space"],
11
+ [8204, "zero-width non-joiner"],
12
+ [8205, "zero-width joiner"],
13
+ [8288, "word joiner"],
14
+ [65279, "zero-width no-break space (interior BOM)"],
15
+ [8234, "left-to-right embedding"],
16
+ [8235, "right-to-left embedding"],
17
+ [8236, "pop directional formatting"],
18
+ [8237, "left-to-right override"],
19
+ [8238, "right-to-left override"],
20
+ [8294, "left-to-right isolate"],
21
+ [8295, "right-to-left isolate"],
22
+ [8296, "first strong isolate"],
23
+ [8297, "pop directional isolate"]
24
+ ]);
25
+ function audit(canonical) {
26
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(canonical);
27
+ const findings = [];
28
+ let line = 1;
29
+ let column = 1;
30
+ for (const ch of text) {
31
+ const cp = ch.codePointAt(0);
32
+ if (cp === void 0) continue;
33
+ if (cp === 10) {
34
+ line++;
35
+ column = 1;
36
+ continue;
37
+ }
38
+ const name = SUSPICIOUS.get(cp);
39
+ if (name !== void 0) findings.push({
40
+ codepoint: cp,
41
+ column,
42
+ line,
43
+ name
44
+ });
45
+ column++;
46
+ }
47
+ return findings;
48
+ }
49
+ //#endregion
50
+ export { audit as t };
51
+
52
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","names":[],"sources":["../src/core/audit.ts"],"sourcesContent":["/**\n * Suspicious-character audit — runs at SIGN time, never at verify time.\n *\n * Verify must always remain a function of canonicalize() alone, never adding\n * stricter rules. Audit is a separate, mutable layer of advice for the author:\n * \"you probably don't want these invisible chars in a published article\".\n */\n\nconst SUSPICIOUS: ReadonlyMap<number, string> = new Map([\n [0x20_0b, 'zero-width space'],\n [0x200c, 'zero-width non-joiner'],\n [0x200d, 'zero-width joiner'],\n [0x20_60, 'word joiner'],\n [0xfeff, 'zero-width no-break space (interior BOM)'],\n [0x20_2a, 'left-to-right embedding'],\n [0x202b, 'right-to-left embedding'],\n [0x202c, 'pop directional formatting'],\n [0x202d, 'left-to-right override'],\n [0x202e, 'right-to-left override'],\n [0x20_66, 'left-to-right isolate'],\n [0x20_67, 'right-to-left isolate'],\n [0x20_68, 'first strong isolate'],\n [0x20_69, 'pop directional isolate'],\n]);\n\nexport type AuditFinding = {\n line: number;\n column: number;\n codepoint: number;\n name: string;\n};\n\nexport function audit(canonical: Uint8Array): AuditFinding[] {\n const text = new TextDecoder('utf-8', { fatal: true }).decode(canonical);\n const findings: AuditFinding[] = [];\n let line = 1;\n let column = 1;\n\n for (const ch of text) {\n const cp = ch.codePointAt(0);\n if (cp === undefined) {\n continue;\n }\n if (cp === 0x0a) {\n line++;\n column = 1;\n continue;\n }\n const name = SUSPICIOUS.get(cp);\n if (name !== undefined) {\n findings.push({ codepoint: cp, column, line, name });\n }\n column++;\n }\n\n return findings;\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,6BAA0C,IAAI,IAAI;CACpD,CAAC,MAAS,kBAAkB;CAC5B,CAAC,MAAQ,uBAAuB;CAChC,CAAC,MAAQ,mBAAmB;CAC5B,CAAC,MAAS,aAAa;CACvB,CAAC,OAAQ,0CAA0C;CACnD,CAAC,MAAS,yBAAyB;CACnC,CAAC,MAAQ,yBAAyB;CAClC,CAAC,MAAQ,4BAA4B;CACrC,CAAC,MAAQ,wBAAwB;CACjC,CAAC,MAAQ,wBAAwB;CACjC,CAAC,MAAS,uBAAuB;CACjC,CAAC,MAAS,uBAAuB;CACjC,CAAC,MAAS,sBAAsB;CAChC,CAAC,MAAS,yBAAyB;AACvC,CAAC;AASD,SAAgB,MAAM,WAAuC;CACzD,MAAM,OAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,SAAS;CACvE,MAAM,WAA2B,CAAC;CAClC,IAAI,OAAO;CACX,IAAI,SAAS;CAEb,KAAK,MAAM,MAAM,MAAM;EACnB,MAAM,KAAK,GAAG,YAAY,CAAC;EAC3B,IAAI,OAAO,KAAA,GACP;EAEJ,IAAI,OAAO,IAAM;GACb;GACA,SAAS;GACT;EACJ;EACA,MAAM,OAAO,WAAW,IAAI,EAAE;EAC9B,IAAI,SAAS,KAAA,GACT,SAAS,KAAK;GAAE,WAAW;GAAI;GAAQ;GAAM;EAAK,CAAC;EAEvD;CACJ;CAEA,OAAO;AACX"}
@@ -0,0 +1,183 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_verify = require("./verify.cjs");
3
+ const require_eip712_schema = require("./eip712-schema.cjs");
4
+ let viem = require("viem");
5
+ let viem_chains = require("viem/chains");
6
+ //#region src/browser/ens.ts
7
+ const client = (0, viem.createPublicClient)({
8
+ chain: viem_chains.mainnet,
9
+ transport: (0, viem.http)("https://ethereum-rpc.publicnode.com")
10
+ });
11
+ /**
12
+ * Reverse-resolve an Ethereum address to its primary ENS name.
13
+ *
14
+ * Pure display concern — never persisted in the attestation file. The signer
15
+ * lib operates on addresses; this is rendered dynamically by web consumers who
16
+ * want a friendlier label.
17
+ *
18
+ * Returns null on any failure (network, no name set, malformed reply).
19
+ */
20
+ async function resolveEnsName(address) {
21
+ try {
22
+ return await client.getEnsName({ address });
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ //#endregion
28
+ //#region src/browser/verify-from-url.ts
29
+ /**
30
+ * End-to-end verify a published article from its public URL.
31
+ *
32
+ * Pipeline:
33
+ * 1. Resolve the article URL → manifest URL (`{url}/proof.json`)
34
+ * 2. Fetch manifest, then content (md), attestation (json), ots (bytes)
35
+ * 3. Recompute SHA-256 of canonical content, recover EIP-712 signer
36
+ * 4. Optionally verify the OTS proof (skipped in browser by default)
37
+ *
38
+ * The same call works in Node, Edge runtime, and modern browsers because the
39
+ * underlying primitives (canonicalize, sha256, recoverTypedDataAddress) are all
40
+ * platform-agnostic.
41
+ */
42
+ async function verifyFromUrl(articleUrl, opts = {}) {
43
+ const fetchFn = opts.fetchFn ?? fetch;
44
+ const manifestUrl = resolveManifestUrl(articleUrl);
45
+ let manifest;
46
+ try {
47
+ manifest = await fetchJson(fetchFn, manifestUrl);
48
+ } catch (error) {
49
+ const details = error.message;
50
+ return {
51
+ authorship: {
52
+ details,
53
+ error: "fetch",
54
+ kind: "failed"
55
+ },
56
+ date: {
57
+ details,
58
+ error: "fetch",
59
+ kind: "failed"
60
+ }
61
+ };
62
+ }
63
+ const base = new URL(manifestUrl);
64
+ let content;
65
+ let attestationJson;
66
+ try {
67
+ [content, attestationJson] = await Promise.all([fetchText(fetchFn, new URL(manifest.content, base).toString()), fetchText(fetchFn, new URL(manifest.attestation, base).toString())]);
68
+ } catch (error) {
69
+ const details = error.message;
70
+ return {
71
+ authorship: {
72
+ details,
73
+ error: "fetch",
74
+ kind: "failed"
75
+ },
76
+ date: {
77
+ details,
78
+ error: "fetch",
79
+ kind: "failed"
80
+ }
81
+ };
82
+ }
83
+ const attestation = require_verify.parse(attestationJson);
84
+ const sig = await require_verify.verifyAttestation({
85
+ attestation,
86
+ content
87
+ });
88
+ if (!sig.ok) return {
89
+ authorship: {
90
+ error: sig.error.kind,
91
+ kind: "failed"
92
+ },
93
+ date: {
94
+ kind: "skipped",
95
+ reason: "opt-out"
96
+ }
97
+ };
98
+ const authorship = {
99
+ kind: "verified",
100
+ signedAt: /* @__PURE__ */ new Date(Number(attestation.claims.publishedAt) * 1e3),
101
+ signerAddress: sig.signerAddress
102
+ };
103
+ if (opts.skipOts) return {
104
+ authorship,
105
+ date: {
106
+ kind: "skipped",
107
+ reason: "opt-out"
108
+ }
109
+ };
110
+ if (!manifest.otsVerifier) return {
111
+ authorship,
112
+ date: {
113
+ kind: "skipped",
114
+ reason: "no-ots-file"
115
+ }
116
+ };
117
+ try {
118
+ const result = await fetchJson(fetchFn, new URL(manifest.otsVerifier, base).toString());
119
+ if (result.ok) return {
120
+ authorship,
121
+ date: {
122
+ bitcoinTime: new Date(result.bitcoinTime),
123
+ kind: "verified"
124
+ }
125
+ };
126
+ if (result.reason === "pending-bitcoin") return {
127
+ authorship,
128
+ date: { kind: "pending" }
129
+ };
130
+ return {
131
+ authorship,
132
+ date: {
133
+ details: result.details,
134
+ error: result.reason,
135
+ kind: "failed"
136
+ }
137
+ };
138
+ } catch (error) {
139
+ return {
140
+ authorship,
141
+ date: {
142
+ details: error.message,
143
+ error: "fetch",
144
+ kind: "failed"
145
+ }
146
+ };
147
+ }
148
+ }
149
+ function resolveManifestUrl(articleUrl) {
150
+ const base = globalThis.location?.origin;
151
+ const url = new URL(articleUrl, base);
152
+ if (!url.pathname.endsWith("/")) url.pathname += "/";
153
+ url.pathname += "proof.json";
154
+ return url.toString();
155
+ }
156
+ async function fetchJson(fetchFn, url) {
157
+ const res = await fetchFn(url);
158
+ if (!res.ok) throw new Error(`GET ${url}: HTTP ${res.status}`);
159
+ return await res.json();
160
+ }
161
+ async function fetchText(fetchFn, url) {
162
+ const res = await fetchFn(url);
163
+ if (!res.ok) throw new Error(`GET ${url}: HTTP ${res.status}`);
164
+ return res.text();
165
+ }
166
+ //#endregion
167
+ exports.ATTESTATION_DOMAIN_V1 = require_eip712_schema.ATTESTATION_DOMAIN_V1;
168
+ exports.ATTESTATION_PRIMARY_TYPE = require_eip712_schema.ATTESTATION_PRIMARY_TYPE;
169
+ exports.ATTESTATION_TYPES_V1 = require_eip712_schema.ATTESTATION_TYPES_V1;
170
+ exports.InvalidContentError = require_verify.InvalidContentError;
171
+ exports.NO_PRIOR_ATTESTATION = require_eip712_schema.NO_PRIOR_ATTESTATION;
172
+ exports.buildAttestationMessage = require_verify.buildAttestationMessage;
173
+ exports.canonicalize = require_verify.canonicalize;
174
+ exports.fromStored = require_verify.fromStored;
175
+ exports.parse = require_verify.parse;
176
+ exports.resolveEnsName = resolveEnsName;
177
+ exports.sha256Hex = require_verify.sha256Hex;
178
+ exports.stringify = require_verify.stringify;
179
+ exports.toStored = require_verify.toStored;
180
+ exports.verifyAttestation = require_verify.verifyAttestation;
181
+ exports.verifyFromUrl = verifyFromUrl;
182
+
183
+ //# sourceMappingURL=browser.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.cjs","names":["mainnet","parse","verifyAttestation","g"],"sources":["../src/browser/ens.ts","../src/browser/verify-from-url.ts"],"sourcesContent":["import { createPublicClient, http } from 'viem';\nimport { mainnet } from 'viem/chains';\n\n// Default viem transport (cloudflare-eth.com) intermittently fails ENS reverse\n// Lookups (returns null even when a primary name is set). publicnode.com is a\n// CORS-friendly community RPC that resolves correctly.\nconst client = createPublicClient({\n chain: mainnet,\n transport: http('https://ethereum-rpc.publicnode.com'),\n});\n\n/**\n * Reverse-resolve an Ethereum address to its primary ENS name.\n *\n * Pure display concern — never persisted in the attestation file. The signer\n * lib operates on addresses; this is rendered dynamically by web consumers who\n * want a friendlier label.\n *\n * Returns null on any failure (network, no name set, malformed reply).\n */\nexport async function resolveEnsName(address: `0x${string}`): Promise<null | string> {\n try {\n return await client.getEnsName({ address });\n } catch {\n return null;\n }\n}\n","import { parse } from '../attestation/serialize.js';\nimport { verifyAttestation } from '../attestation/verify.js';\nimport { type AuthorshipState, type DateState, type ProofManifest } from './types.js';\n\nexport type VerifyFromUrlOptions = {\n /**\n * Skip Bitcoin/OTS verification entirely. Default false — verifyFromUrl\n * lazy-loads `javascript-opentimestamps` (~200kB) and validates the proof\n * against a public Bitcoin block API. Fails open: bundling/polyfill issues\n * fall back to `skipped` rather than throwing.\n */\n skipOts?: boolean;\n /**\n * Custom fetch (useful for tests or proxies). Defaults to global fetch.\n */\n fetchFn?: typeof fetch;\n};\n\nexport type VerifyFromUrlReport = {\n authorship: AuthorshipState;\n date: DateState;\n};\n\n/**\n * End-to-end verify a published article from its public URL.\n *\n * Pipeline:\n * 1. Resolve the article URL → manifest URL (`{url}/proof.json`)\n * 2. Fetch manifest, then content (md), attestation (json), ots (bytes)\n * 3. Recompute SHA-256 of canonical content, recover EIP-712 signer\n * 4. Optionally verify the OTS proof (skipped in browser by default)\n *\n * The same call works in Node, Edge runtime, and modern browsers because the\n * underlying primitives (canonicalize, sha256, recoverTypedDataAddress) are all\n * platform-agnostic.\n */\nexport async function verifyFromUrl(\n articleUrl: string,\n opts: VerifyFromUrlOptions = {},\n): Promise<VerifyFromUrlReport> {\n const fetchFn = opts.fetchFn ?? fetch;\n\n const manifestUrl = resolveManifestUrl(articleUrl);\n let manifest: ProofManifest;\n try {\n manifest = await fetchJson<ProofManifest>(fetchFn, manifestUrl);\n } catch (error) {\n const details = (error as Error).message;\n return {\n authorship: { details, error: 'fetch', kind: 'failed' },\n date: { details, error: 'fetch', kind: 'failed' },\n };\n }\n\n const base = new URL(manifestUrl);\n let content: string;\n let attestationJson: string;\n try {\n [content, attestationJson] = await Promise.all([\n fetchText(fetchFn, new URL(manifest.content, base).toString()),\n fetchText(fetchFn, new URL(manifest.attestation, base).toString()),\n ]);\n } catch (error) {\n const details = (error as Error).message;\n return {\n authorship: { details, error: 'fetch', kind: 'failed' },\n date: { details, error: 'fetch', kind: 'failed' },\n };\n }\n\n const attestation = parse(attestationJson);\n const sig = await verifyAttestation({ attestation, content });\n\n if (!sig.ok) {\n return {\n authorship: { error: sig.error.kind, kind: 'failed' },\n date: { kind: 'skipped', reason: 'opt-out' },\n };\n }\n\n const authorship: AuthorshipState = {\n kind: 'verified',\n signedAt: new Date(Number(attestation.claims.publishedAt) * 1000),\n signerAddress: sig.signerAddress,\n };\n\n if (opts.skipOts) {\n return { authorship, date: { kind: 'skipped', reason: 'opt-out' } };\n }\n\n // OTS verification needs Node-only deps (fs, crypto) so it runs on the\n // Server. The manifest may point to a verifier endpoint that returns the\n // Result as JSON. If absent, browser falls back to \"skipped\".\n if (!manifest.otsVerifier) {\n return { authorship, date: { kind: 'skipped', reason: 'no-ots-file' } };\n }\n\n try {\n const verifierUrl = new URL(manifest.otsVerifier, base).toString();\n const result = await fetchJson<OtsVerifierResponse>(fetchFn, verifierUrl);\n if (result.ok) {\n return {\n authorship,\n date: { bitcoinTime: new Date(result.bitcoinTime), kind: 'verified' },\n };\n }\n if (result.reason === 'pending-bitcoin') {\n return { authorship, date: { kind: 'pending' } };\n }\n return {\n authorship,\n date: { details: result.details, error: result.reason, kind: 'failed' },\n };\n } catch (error) {\n return {\n authorship,\n date: {\n details: (error as Error).message,\n error: 'fetch',\n kind: 'failed',\n },\n };\n }\n}\n\ntype OtsVerifierResponse =\n | {\n ok: false;\n reason: 'digest-mismatch' | 'invalid-proof' | 'pending-bitcoin';\n details?: string;\n }\n | { ok: true; bitcoinTime: string };\n\nfunction resolveManifestUrl(articleUrl: string): string {\n // Accept absolute URLs and relative paths (browser uses location.origin).\n const g = globalThis as { location?: { origin: string } };\n const base = g.location?.origin;\n const url = new URL(articleUrl, base);\n if (!url.pathname.endsWith('/')) {\n url.pathname += '/';\n }\n url.pathname += 'proof.json';\n return url.toString();\n}\n\nasync function fetchJson<T>(fetchFn: typeof fetch, url: string): Promise<T> {\n const res = await fetchFn(url);\n if (!res.ok) {\n throw new Error(`GET ${url}: HTTP ${res.status}`);\n }\n return (await res.json()) as T;\n}\n\nasync function fetchText(fetchFn: typeof fetch, url: string): Promise<string> {\n const res = await fetchFn(url);\n if (!res.ok) {\n throw new Error(`GET ${url}: HTTP ${res.status}`);\n }\n return res.text();\n}\n"],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import { _ as InvalidContentError, a as stringify, c as buildAttestationMessage, d as SignedAttestation, f as StoredAttestation, g as VerifyResult, h as VerifyOk, i as parse, m as VerifyFail, n as verifyAttestation, o as toStored, p as VerifyError, r as fromStored, s as CreateAttestationInput, t as VerifyInput, v as canonicalize } from "./verify.cjs";
2
+ import { a as ArticleSubject, i as ArticleClaims, n as ATTESTATION_PRIMARY_TYPE, o as AttestationMessage, r as ATTESTATION_TYPES_V1, s as NO_PRIOR_ATTESTATION, t as ATTESTATION_DOMAIN_V1 } from "./eip712-schema.cjs";
3
+ //#region src/core/sha256.d.ts
4
+ /**
5
+ * SHA-256 of bytes, returned as bare hex (no 0x prefix).
6
+ *
7
+ * Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,
8
+ * Deno, and every modern browser. This is the only hash primitive used by the
9
+ * package, so swapping the implementation here is the single point of change.
10
+ */
11
+ declare function sha256Hex(bytes: Uint8Array): string;
12
+ //#endregion
13
+ //#region src/browser/ens.d.ts
14
+ /**
15
+ * Reverse-resolve an Ethereum address to its primary ENS name.
16
+ *
17
+ * Pure display concern — never persisted in the attestation file. The signer
18
+ * lib operates on addresses; this is rendered dynamically by web consumers who
19
+ * want a friendlier label.
20
+ *
21
+ * Returns null on any failure (network, no name set, malformed reply).
22
+ */
23
+ declare function resolveEnsName(address: `0x${string}`): Promise<null | string>;
24
+ //#endregion
25
+ //#region src/browser/types.d.ts
26
+ type ProofManifest = {
27
+ schemaVersion: number;
28
+ slug: string;
29
+ content: string;
30
+ attestation: string;
31
+ ots: string;
32
+ /**
33
+ * Optional URL of a server-side OTS verifier. The browser cannot validate
34
+ * OpenTimestamps proofs locally (the lib needs Node `fs` + `crypto`), so
35
+ * publishers expose this endpoint to delegate the Bitcoin check.
36
+ *
37
+ * Returns: { ok: true; bitcoinTime: ISO } | { ok: false; reason; details? }
38
+ */
39
+ otsVerifier?: string;
40
+ };
41
+ type AuthorshipState = {
42
+ kind: 'failed';
43
+ error: 'fetch' | VerifyError['kind'];
44
+ details?: string;
45
+ } | {
46
+ kind: 'fetching';
47
+ } | {
48
+ kind: 'idle';
49
+ } | {
50
+ kind: 'verified';
51
+ signerAddress: `0x${string}`;
52
+ signerEns?: string;
53
+ signedAt: Date;
54
+ } | {
55
+ kind: 'verifying';
56
+ };
57
+ type DateState = {
58
+ kind: 'failed';
59
+ error: 'digest-mismatch' | 'fetch' | 'invalid-proof';
60
+ details?: string;
61
+ } | {
62
+ kind: 'fetching';
63
+ } | {
64
+ kind: 'idle';
65
+ } | {
66
+ kind: 'pending';
67
+ } | {
68
+ kind: 'skipped';
69
+ reason: 'browser-runtime' | 'no-ots-file' | 'opt-out';
70
+ } | {
71
+ kind: 'verified';
72
+ bitcoinTime: Date;
73
+ } | {
74
+ kind: 'verifying';
75
+ };
76
+ //#endregion
77
+ //#region src/browser/verify-from-url.d.ts
78
+ type VerifyFromUrlOptions = {
79
+ /**
80
+ * Skip Bitcoin/OTS verification entirely. Default false — verifyFromUrl
81
+ * lazy-loads `javascript-opentimestamps` (~200kB) and validates the proof
82
+ * against a public Bitcoin block API. Fails open: bundling/polyfill issues
83
+ * fall back to `skipped` rather than throwing.
84
+ */
85
+ skipOts?: boolean;
86
+ /**
87
+ * Custom fetch (useful for tests or proxies). Defaults to global fetch.
88
+ */
89
+ fetchFn?: typeof fetch;
90
+ };
91
+ type VerifyFromUrlReport = {
92
+ authorship: AuthorshipState;
93
+ date: DateState;
94
+ };
95
+ /**
96
+ * End-to-end verify a published article from its public URL.
97
+ *
98
+ * Pipeline:
99
+ * 1. Resolve the article URL → manifest URL (`{url}/proof.json`)
100
+ * 2. Fetch manifest, then content (md), attestation (json), ots (bytes)
101
+ * 3. Recompute SHA-256 of canonical content, recover EIP-712 signer
102
+ * 4. Optionally verify the OTS proof (skipped in browser by default)
103
+ *
104
+ * The same call works in Node, Edge runtime, and modern browsers because the
105
+ * underlying primitives (canonicalize, sha256, recoverTypedDataAddress) are all
106
+ * platform-agnostic.
107
+ */
108
+ declare function verifyFromUrl(articleUrl: string, opts?: VerifyFromUrlOptions): Promise<VerifyFromUrlReport>;
109
+ //#endregion
110
+ export { ATTESTATION_DOMAIN_V1, ATTESTATION_PRIMARY_TYPE, ATTESTATION_TYPES_V1, type ArticleClaims, type ArticleSubject, type AttestationMessage, type AuthorshipState, type CreateAttestationInput, type DateState, InvalidContentError, NO_PRIOR_ATTESTATION, type ProofManifest, type SignedAttestation, type StoredAttestation, type VerifyError, type VerifyFail, type VerifyFromUrlOptions, type VerifyFromUrlReport, type VerifyInput, type VerifyOk, type VerifyResult, buildAttestationMessage, canonicalize, fromStored, parse, resolveEnsName, sha256Hex, stringify, toStored, verifyAttestation, verifyFromUrl };
111
+ //# sourceMappingURL=browser.d.cts.map
@@ -0,0 +1,111 @@
1
+ import { _ as InvalidContentError, a as stringify, c as buildAttestationMessage, d as SignedAttestation, f as StoredAttestation, g as VerifyResult, h as VerifyOk, i as parse, m as VerifyFail, n as verifyAttestation, o as toStored, p as VerifyError, r as fromStored, s as CreateAttestationInput, t as VerifyInput, v as canonicalize } from "./verify.js";
2
+ import { a as ArticleSubject, i as ArticleClaims, n as ATTESTATION_PRIMARY_TYPE, o as AttestationMessage, r as ATTESTATION_TYPES_V1, s as NO_PRIOR_ATTESTATION, t as ATTESTATION_DOMAIN_V1 } from "./eip712-schema.js";
3
+ //#region src/core/sha256.d.ts
4
+ /**
5
+ * SHA-256 of bytes, returned as bare hex (no 0x prefix).
6
+ *
7
+ * Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,
8
+ * Deno, and every modern browser. This is the only hash primitive used by the
9
+ * package, so swapping the implementation here is the single point of change.
10
+ */
11
+ declare function sha256Hex(bytes: Uint8Array): string;
12
+ //#endregion
13
+ //#region src/browser/ens.d.ts
14
+ /**
15
+ * Reverse-resolve an Ethereum address to its primary ENS name.
16
+ *
17
+ * Pure display concern — never persisted in the attestation file. The signer
18
+ * lib operates on addresses; this is rendered dynamically by web consumers who
19
+ * want a friendlier label.
20
+ *
21
+ * Returns null on any failure (network, no name set, malformed reply).
22
+ */
23
+ declare function resolveEnsName(address: `0x${string}`): Promise<null | string>;
24
+ //#endregion
25
+ //#region src/browser/types.d.ts
26
+ type ProofManifest = {
27
+ schemaVersion: number;
28
+ slug: string;
29
+ content: string;
30
+ attestation: string;
31
+ ots: string;
32
+ /**
33
+ * Optional URL of a server-side OTS verifier. The browser cannot validate
34
+ * OpenTimestamps proofs locally (the lib needs Node `fs` + `crypto`), so
35
+ * publishers expose this endpoint to delegate the Bitcoin check.
36
+ *
37
+ * Returns: { ok: true; bitcoinTime: ISO } | { ok: false; reason; details? }
38
+ */
39
+ otsVerifier?: string;
40
+ };
41
+ type AuthorshipState = {
42
+ kind: 'failed';
43
+ error: 'fetch' | VerifyError['kind'];
44
+ details?: string;
45
+ } | {
46
+ kind: 'fetching';
47
+ } | {
48
+ kind: 'idle';
49
+ } | {
50
+ kind: 'verified';
51
+ signerAddress: `0x${string}`;
52
+ signerEns?: string;
53
+ signedAt: Date;
54
+ } | {
55
+ kind: 'verifying';
56
+ };
57
+ type DateState = {
58
+ kind: 'failed';
59
+ error: 'digest-mismatch' | 'fetch' | 'invalid-proof';
60
+ details?: string;
61
+ } | {
62
+ kind: 'fetching';
63
+ } | {
64
+ kind: 'idle';
65
+ } | {
66
+ kind: 'pending';
67
+ } | {
68
+ kind: 'skipped';
69
+ reason: 'browser-runtime' | 'no-ots-file' | 'opt-out';
70
+ } | {
71
+ kind: 'verified';
72
+ bitcoinTime: Date;
73
+ } | {
74
+ kind: 'verifying';
75
+ };
76
+ //#endregion
77
+ //#region src/browser/verify-from-url.d.ts
78
+ type VerifyFromUrlOptions = {
79
+ /**
80
+ * Skip Bitcoin/OTS verification entirely. Default false — verifyFromUrl
81
+ * lazy-loads `javascript-opentimestamps` (~200kB) and validates the proof
82
+ * against a public Bitcoin block API. Fails open: bundling/polyfill issues
83
+ * fall back to `skipped` rather than throwing.
84
+ */
85
+ skipOts?: boolean;
86
+ /**
87
+ * Custom fetch (useful for tests or proxies). Defaults to global fetch.
88
+ */
89
+ fetchFn?: typeof fetch;
90
+ };
91
+ type VerifyFromUrlReport = {
92
+ authorship: AuthorshipState;
93
+ date: DateState;
94
+ };
95
+ /**
96
+ * End-to-end verify a published article from its public URL.
97
+ *
98
+ * Pipeline:
99
+ * 1. Resolve the article URL → manifest URL (`{url}/proof.json`)
100
+ * 2. Fetch manifest, then content (md), attestation (json), ots (bytes)
101
+ * 3. Recompute SHA-256 of canonical content, recover EIP-712 signer
102
+ * 4. Optionally verify the OTS proof (skipped in browser by default)
103
+ *
104
+ * The same call works in Node, Edge runtime, and modern browsers because the
105
+ * underlying primitives (canonicalize, sha256, recoverTypedDataAddress) are all
106
+ * platform-agnostic.
107
+ */
108
+ declare function verifyFromUrl(articleUrl: string, opts?: VerifyFromUrlOptions): Promise<VerifyFromUrlReport>;
109
+ //#endregion
110
+ export { ATTESTATION_DOMAIN_V1, ATTESTATION_PRIMARY_TYPE, ATTESTATION_TYPES_V1, type ArticleClaims, type ArticleSubject, type AttestationMessage, type AuthorshipState, type CreateAttestationInput, type DateState, InvalidContentError, NO_PRIOR_ATTESTATION, type ProofManifest, type SignedAttestation, type StoredAttestation, type VerifyError, type VerifyFail, type VerifyFromUrlOptions, type VerifyFromUrlReport, type VerifyInput, type VerifyOk, type VerifyResult, buildAttestationMessage, canonicalize, fromStored, parse, resolveEnsName, sha256Hex, stringify, toStored, verifyAttestation, verifyFromUrl };
111
+ //# sourceMappingURL=browser.d.ts.map