@jterrazz/attestation 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +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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"}
1
+ {"version":3,"file":"browser.cjs","names":["createPublicClient","mainnet","http","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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"}
package/dist/cli.cjs CHANGED
@@ -62,7 +62,8 @@ async function runSign(args) {
62
62
  process.stdout.write(`${checkmark()} Wrote ${attestationPath}\n`);
63
63
  if (!args.skipStamp) {
64
64
  process.stdout.write(`${fmt.info("→")} Submitting to OpenTimestamps calendars…\n`);
65
- const otsBytes = await require_sign_flow.stampDigest(hexToBytes$1(message.subject.contentDigest));
65
+ const digest = hexToBytes$1(message.subject.contentDigest);
66
+ const otsBytes = await require_sign_flow.stampDigest(digest);
66
67
  const otsPath = (0, node_path.join)(dir, `${baseName}.ots`);
67
68
  await (0, node_fs_promises.writeFile)(otsPath, Buffer.from(otsBytes));
68
69
  process.stdout.write(`${checkmark()} Wrote ${otsPath} (${otsBytes.length} bytes, calendar-only)\n`);
@@ -113,7 +114,8 @@ async function runVerify(args) {
113
114
  process.stdout.write(`${fmt.dim("· OTS verification skipped")}\n`);
114
115
  return true;
115
116
  }
116
- const otsResult = await require_sign_flow.verifyOts(hexToBytes(attestation.subject.contentDigest), sources.otsBytes);
117
+ const digest = hexToBytes(attestation.subject.contentDigest);
118
+ const otsResult = await require_sign_flow.verifyOts(digest, sources.otsBytes);
117
119
  if (!otsResult.ok) {
118
120
  process.stdout.write(`${crossmark()} OTS: ${otsResult.reason}`);
119
121
  if (otsResult.details) process.stdout.write(` — ${otsResult.details}`);
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","names":["canonicalize","audit","buildAttestationMessage","signViaBrowser","stringify","stampDigest","hexToBytes","upgradeProof","parse","verifyAttestation","verifyOts"],"sources":["../src/cli/io.ts","../src/cli/sign.ts","../src/cli/upgrade.ts","../src/cli/verify.ts","../src/cli/index.ts","../src/cli.ts"],"sourcesContent":["/* ANSI helpers — kept tiny so we don't pull in chalk. */\n\nconst supportsColor = process.stdout.isTTY === true && !process.env.NO_COLOR;\n\nexport const fmt = {\n bold: (s: string) => (supportsColor ? `\\x1b[1m${s}\\x1b[0m` : s),\n dim: (s: string) => (supportsColor ? `\\x1b[2m${s}\\x1b[0m` : s),\n fail: (s: string) => (supportsColor ? `\\x1b[31m${s}\\x1b[0m` : s),\n info: (s: string) => (supportsColor ? `\\x1b[36m${s}\\x1b[0m` : s),\n ok: (s: string) => (supportsColor ? `\\x1b[32m${s}\\x1b[0m` : s),\n warn: (s: string) => (supportsColor ? `\\x1b[33m${s}\\x1b[0m` : s),\n};\n\nexport function checkmark(): string {\n return fmt.ok('✓');\n}\n\nexport function crossmark(): string {\n return fmt.fail('✗');\n}\n","import { readFile, writeFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { buildAttestationMessage } from '../attestation/create.js';\nimport { stringify } from '../attestation/serialize.js';\nimport { type SignedAttestation } from '../attestation/types.js';\nimport { audit } from '../core/audit.js';\nimport { canonicalize } from '../core/canonicalize.js';\nimport { signViaBrowser } from '../eth/sign-flow.js';\nimport { stampDigest } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type SignArgs = {\n file: string;\n title: string;\n slug: string;\n locale?: string;\n publishedAt?: string;\n revision?: number;\n priorAttestation?: `0x${string}`;\n skipStamp?: boolean;\n skipAudit?: boolean;\n};\n\nexport async function runSign(args: SignArgs): Promise<void> {\n const content = await readFile(args.file, 'utf8');\n const locale = args.locale ?? deriveLocale(args.file);\n const publishedAt = args.publishedAt ? new Date(args.publishedAt) : new Date();\n\n const canonical = canonicalize(content);\n process.stdout.write(`${checkmark()} Canonicalized ${canonical.length} bytes (${locale})\\n`);\n\n if (!args.skipAudit) {\n const findings = audit(canonical);\n if (findings.length > 0) {\n process.stdout.write(`${fmt.warn('!')} ${findings.length} suspicious char(s) found:\\n`);\n for (const f of findings) {\n process.stdout.write(\n ` line ${f.line}, col ${f.column}: U+${f.codepoint\n .toString(16)\n .toUpperCase()\n .padStart(4, '0')} (${f.name})\\n`,\n );\n }\n process.stdout.write(` Pass --skip-audit to sign anyway.\\n`);\n throw new Error('Audit failed: clean up suspicious characters first.');\n }\n }\n\n const message = buildAttestationMessage({\n content,\n locale,\n priorAttestation: args.priorAttestation,\n publishedAt,\n revision: args.revision,\n slug: args.slug,\n title: args.title,\n });\n\n process.stdout.write(`${fmt.info('→')} Content digest: ${message.subject.contentDigest}\\n`);\n process.stdout.write(`${fmt.info('→')} Opening browser to sign with your wallet…\\n`);\n\n const { signature, signerAddress } = await signViaBrowser({ message });\n const signed: SignedAttestation = { ...message, signature, signerAddress };\n\n process.stdout.write(`${checkmark()} Signed by ${fmt.bold(signerAddress)}\\n`);\n\n const baseName = basename(args.file, extname(args.file));\n const dir = dirname(args.file);\n const attestationPath = join(dir, `${baseName}.attestation.json`);\n await writeFile(attestationPath, stringify(signed), 'utf8');\n process.stdout.write(`${checkmark()} Wrote ${attestationPath}\\n`);\n\n if (!args.skipStamp) {\n process.stdout.write(`${fmt.info('→')} Submitting to OpenTimestamps calendars…\\n`);\n const digest = hexToBytes(message.subject.contentDigest);\n const otsBytes = await stampDigest(digest);\n const otsPath = join(dir, `${baseName}.ots`);\n await writeFile(otsPath, Buffer.from(otsBytes));\n process.stdout.write(\n `${checkmark()} Wrote ${otsPath} (${otsBytes.length} bytes, calendar-only)\\n`,\n );\n process.stdout.write(\n ` ${fmt.dim('Run `attestation upgrade` in ~24h to anchor in Bitcoin.')}\\n`,\n );\n }\n}\n\nfunction deriveLocale(file: string): string {\n const name = basename(file, extname(file));\n if (/^[a-z]{2}$/.test(name)) {\n return name;\n }\n throw new Error(\n `Cannot derive locale from filename \"${basename(file)}\". Pass --locale explicitly.`,\n );\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { readFile, writeFile } from 'node:fs/promises';\n\nimport { upgradeProof } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type UpgradeArgs = {\n files: string[];\n};\n\nexport async function runUpgrade(args: UpgradeArgs): Promise<void> {\n if (args.files.length === 0) {\n throw new Error('No .ots files provided.');\n }\n\n for (const path of args.files) {\n process.stdout.write(`${fmt.info('→')} ${path}\\n`);\n const before = await readFile(path);\n const { bytes, upgraded } = await upgradeProof(new Uint8Array(before));\n\n if (upgraded) {\n await writeFile(path, Buffer.from(bytes));\n process.stdout.write(\n ` ${checkmark()} Upgraded to Bitcoin attestation (${bytes.length} bytes)\\n`,\n );\n } else {\n process.stdout.write(\n ` ${fmt.dim('· No new attestation available yet, leaving file untouched.')}\\n`,\n );\n }\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { parse } from '../attestation/serialize.js';\nimport { verifyAttestation } from '../attestation/verify.js';\nimport { verifyOts } from '../ots/verify.js';\nimport { checkmark, crossmark, fmt } from './io.js';\n\nexport type VerifyArgs = {\n /** A URL to an article, or a path to a local markdown file. */\n target: string;\n /** Skip OTS Bitcoin verification (signature-only). */\n skipOts?: boolean;\n};\n\ntype Sources = {\n content: string;\n attestationJson: string;\n otsBytes: null | Uint8Array;\n};\n\nexport async function runVerify(args: VerifyArgs): Promise<boolean> {\n const sources = await loadSources(args);\n\n const attestation = parse(sources.attestationJson);\n const sigResult = await verifyAttestation({ attestation, content: sources.content });\n\n if (!sigResult.ok) {\n printSignatureFailure(sigResult.error);\n return false;\n }\n\n process.stdout.write(`${checkmark()} Content digest matches signature\\n`);\n process.stdout.write(\n `${checkmark()} Signature valid — signed by ${fmt.bold(sigResult.signerAddress)}\\n`,\n );\n\n if (args.skipOts || sources.otsBytes === null) {\n process.stdout.write(`${fmt.dim('· OTS verification skipped')}\\n`);\n return true;\n }\n\n const digest = hexToBytes(attestation.subject.contentDigest);\n const otsResult = await verifyOts(digest, sources.otsBytes);\n\n if (!otsResult.ok) {\n process.stdout.write(`${crossmark()} OTS: ${otsResult.reason}`);\n if (otsResult.details) {\n process.stdout.write(` — ${otsResult.details}`);\n }\n process.stdout.write(`\\n`);\n return otsResult.reason === 'pending-bitcoin'; // Pending is not a hard fail\n }\n\n process.stdout.write(\n `${checkmark()} Bitcoin timestamp confirmed at ${otsResult.bitcoinBlockTime.toISOString()}\\n`,\n );\n return true;\n}\n\nasync function loadSources(args: VerifyArgs): Promise<Sources> {\n if (/^https?:\\/\\//.test(args.target)) {\n return loadFromUrl(args.target);\n }\n return loadFromFile(args.target);\n}\n\nasync function loadFromFile(filePath: string): Promise<Sources> {\n const content = await readFile(filePath, 'utf8');\n const dir = dirname(filePath);\n const base = basename(filePath, extname(filePath));\n const attestationJson = await readFile(join(dir, `${base}.attestation.json`), 'utf8');\n let otsBytes: null | Uint8Array = null;\n try {\n const buf = await readFile(join(dir, `${base}.ots`));\n otsBytes = new Uint8Array(buf);\n } catch {\n otsBytes = null;\n }\n return { attestationJson, content, otsBytes };\n}\n\nasync function loadFromUrl(url: string): Promise<Sources> {\n const proofManifestUrl = await resolveManifestUrl(url);\n const manifest = await fetchJson<ProofManifest>(proofManifestUrl);\n\n const base = new URL(proofManifestUrl);\n const [content, attestationJson, otsBuf] = await Promise.all([\n fetchText(new URL(manifest.content, base).toString()),\n fetchText(new URL(manifest.attestation, base).toString()),\n fetchBytes(new URL(manifest.ots, base).toString()).catch(() => null),\n ]);\n return { attestationJson, content, otsBytes: otsBuf };\n}\n\ntype ProofManifest = {\n schemaVersion: number;\n slug: string;\n content: string;\n attestation: string;\n ots: string;\n};\n\nasync function resolveManifestUrl(articleUrl: string): Promise<string> {\n // Convention: append /proof.json to the article URL (trailing slash tolerated).\n const url = new URL(articleUrl);\n if (!url.pathname.endsWith('/')) {\n url.pathname += '/';\n }\n url.pathname += 'proof.json';\n return url.toString();\n}\n\nasync function fetchJson<T>(url: string): Promise<T> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return (await res.json()) as T;\n}\n\nasync function fetchText(url: string): Promise<string> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return res.text();\n}\n\nasync function fetchBytes(url: string): Promise<Uint8Array> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return new Uint8Array(await res.arrayBuffer());\n}\n\nfunction printSignatureFailure(error: Record<string, unknown> & { kind: string }): void {\n process.stdout.write(`${crossmark()} ${fmt.fail('Verification failed')}: ${error.kind}\\n`);\n for (const [k, v] of Object.entries(error)) {\n if (k === 'kind') {\n continue;\n }\n process.stdout.write(` ${k}: ${String(v)}\\n`);\n }\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { parseArgs } from 'node:util';\n\nimport { fmt } from './io.js';\nimport { runSign } from './sign.js';\nimport { runUpgrade } from './upgrade.js';\nimport { runVerify } from './verify.js';\n\nconst HELP = `${fmt.bold('attestation')} — sign articles with EIP-712 + Bitcoin (OpenTimestamps).\n\nUsage:\n attestation sign <file> --title <t> --slug <s> [--published-at <iso>]\n attestation verify <url|file> [--skip-ots]\n attestation upgrade <ots-file> [<ots-file> …]\n\nRun \\`attestation <command> --help\\` for command-specific options.\n`;\n\nexport async function runCli(argv: string[]): Promise<number> {\n const [command, ...rest] = argv;\n\n if (command === undefined || command === '-h' || command === '--help' || command === 'help') {\n process.stdout.write(HELP);\n return 0;\n }\n\n try {\n if (command === 'sign') {\n return await dispatchSign(rest);\n }\n if (command === 'verify') {\n return await dispatchVerify(rest);\n }\n if (command === 'upgrade') {\n return await dispatchUpgrade(rest);\n }\n process.stderr.write(`Unknown command: ${command}\\n${HELP}`);\n return 2;\n } catch (error) {\n process.stderr.write(`${fmt.fail('Error:')} ${(error as Error).message}\\n`);\n return 1;\n }\n}\n\nasync function dispatchSign(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n locale: { type: 'string' },\n 'prior-attestation': { type: 'string' },\n 'published-at': { type: 'string' },\n revision: { type: 'string' },\n 'skip-audit': { type: 'boolean' },\n 'skip-stamp': { type: 'boolean' },\n slug: { type: 'string' },\n title: { type: 'string' },\n },\n });\n\n const file = positionals[0];\n if (!file) {\n throw new Error('Missing required <file> argument.');\n }\n if (!values.title) {\n throw new Error('Missing required --title.');\n }\n if (!values.slug) {\n throw new Error('Missing required --slug.');\n }\n\n await runSign({\n file,\n locale: values.locale,\n priorAttestation: values['prior-attestation'] as `0x${string}` | undefined,\n publishedAt: values['published-at'],\n revision: values.revision ? Number(values.revision) : undefined,\n skipAudit: values['skip-audit'],\n skipStamp: values['skip-stamp'],\n slug: values.slug,\n title: values.title,\n });\n return 0;\n}\n\nasync function dispatchVerify(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n 'skip-ots': { type: 'boolean' },\n },\n });\n\n const target = positionals[0];\n if (!target) {\n throw new Error('Missing required <url|file> argument.');\n }\n\n const ok = await runVerify({ skipOts: values['skip-ots'], target });\n return ok ? 0 : 1;\n}\n\nasync function dispatchUpgrade(argv: string[]): Promise<number> {\n const { positionals } = parseArgs({ allowPositionals: true, args: argv, options: {} });\n await runUpgrade({ files: positionals });\n return 0;\n}\n","import { runCli } from './cli/index.js';\n\nvoid runCli(process.argv.slice(2)).then((code) => process.exit(code));\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","names":["readFile","canonicalize","audit","buildAttestationMessage","signViaBrowser","basename","extname","dirname","join","writeFile","stringify","hexToBytes","stampDigest","readFile","upgradeProof","writeFile","parse","verifyAttestation","verifyOts","readFile","dirname","basename","extname","join","parseArgs"],"sources":["../src/cli/io.ts","../src/cli/sign.ts","../src/cli/upgrade.ts","../src/cli/verify.ts","../src/cli/index.ts","../src/cli.ts"],"sourcesContent":["/* ANSI helpers — kept tiny so we don't pull in chalk. */\n\nconst supportsColor = process.stdout.isTTY === true && !process.env.NO_COLOR;\n\nexport const fmt = {\n bold: (s: string) => (supportsColor ? `\\x1b[1m${s}\\x1b[0m` : s),\n dim: (s: string) => (supportsColor ? `\\x1b[2m${s}\\x1b[0m` : s),\n fail: (s: string) => (supportsColor ? `\\x1b[31m${s}\\x1b[0m` : s),\n info: (s: string) => (supportsColor ? `\\x1b[36m${s}\\x1b[0m` : s),\n ok: (s: string) => (supportsColor ? `\\x1b[32m${s}\\x1b[0m` : s),\n warn: (s: string) => (supportsColor ? `\\x1b[33m${s}\\x1b[0m` : s),\n};\n\nexport function checkmark(): string {\n return fmt.ok('✓');\n}\n\nexport function crossmark(): string {\n return fmt.fail('✗');\n}\n","import { readFile, writeFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { buildAttestationMessage } from '../attestation/create.js';\nimport { stringify } from '../attestation/serialize.js';\nimport { type SignedAttestation } from '../attestation/types.js';\nimport { audit } from '../core/audit.js';\nimport { canonicalize } from '../core/canonicalize.js';\nimport { signViaBrowser } from '../eth/sign-flow.js';\nimport { stampDigest } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type SignArgs = {\n file: string;\n title: string;\n slug: string;\n locale?: string;\n publishedAt?: string;\n revision?: number;\n priorAttestation?: `0x${string}`;\n skipStamp?: boolean;\n skipAudit?: boolean;\n};\n\nexport async function runSign(args: SignArgs): Promise<void> {\n const content = await readFile(args.file, 'utf8');\n const locale = args.locale ?? deriveLocale(args.file);\n const publishedAt = args.publishedAt ? new Date(args.publishedAt) : new Date();\n\n const canonical = canonicalize(content);\n process.stdout.write(`${checkmark()} Canonicalized ${canonical.length} bytes (${locale})\\n`);\n\n if (!args.skipAudit) {\n const findings = audit(canonical);\n if (findings.length > 0) {\n process.stdout.write(`${fmt.warn('!')} ${findings.length} suspicious char(s) found:\\n`);\n for (const f of findings) {\n process.stdout.write(\n ` line ${f.line}, col ${f.column}: U+${f.codepoint\n .toString(16)\n .toUpperCase()\n .padStart(4, '0')} (${f.name})\\n`,\n );\n }\n process.stdout.write(` Pass --skip-audit to sign anyway.\\n`);\n throw new Error('Audit failed: clean up suspicious characters first.');\n }\n }\n\n const message = buildAttestationMessage({\n content,\n locale,\n priorAttestation: args.priorAttestation,\n publishedAt,\n revision: args.revision,\n slug: args.slug,\n title: args.title,\n });\n\n process.stdout.write(`${fmt.info('→')} Content digest: ${message.subject.contentDigest}\\n`);\n process.stdout.write(`${fmt.info('→')} Opening browser to sign with your wallet…\\n`);\n\n const { signature, signerAddress } = await signViaBrowser({ message });\n const signed: SignedAttestation = { ...message, signature, signerAddress };\n\n process.stdout.write(`${checkmark()} Signed by ${fmt.bold(signerAddress)}\\n`);\n\n const baseName = basename(args.file, extname(args.file));\n const dir = dirname(args.file);\n const attestationPath = join(dir, `${baseName}.attestation.json`);\n await writeFile(attestationPath, stringify(signed), 'utf8');\n process.stdout.write(`${checkmark()} Wrote ${attestationPath}\\n`);\n\n if (!args.skipStamp) {\n process.stdout.write(`${fmt.info('→')} Submitting to OpenTimestamps calendars…\\n`);\n const digest = hexToBytes(message.subject.contentDigest);\n const otsBytes = await stampDigest(digest);\n const otsPath = join(dir, `${baseName}.ots`);\n await writeFile(otsPath, Buffer.from(otsBytes));\n process.stdout.write(\n `${checkmark()} Wrote ${otsPath} (${otsBytes.length} bytes, calendar-only)\\n`,\n );\n process.stdout.write(\n ` ${fmt.dim('Run `attestation upgrade` in ~24h to anchor in Bitcoin.')}\\n`,\n );\n }\n}\n\nfunction deriveLocale(file: string): string {\n const name = basename(file, extname(file));\n if (/^[a-z]{2}$/.test(name)) {\n return name;\n }\n throw new Error(\n `Cannot derive locale from filename \"${basename(file)}\". Pass --locale explicitly.`,\n );\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { readFile, writeFile } from 'node:fs/promises';\n\nimport { upgradeProof } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type UpgradeArgs = {\n files: string[];\n};\n\nexport async function runUpgrade(args: UpgradeArgs): Promise<void> {\n if (args.files.length === 0) {\n throw new Error('No .ots files provided.');\n }\n\n for (const path of args.files) {\n process.stdout.write(`${fmt.info('→')} ${path}\\n`);\n const before = await readFile(path);\n const { bytes, upgraded } = await upgradeProof(new Uint8Array(before));\n\n if (upgraded) {\n await writeFile(path, Buffer.from(bytes));\n process.stdout.write(\n ` ${checkmark()} Upgraded to Bitcoin attestation (${bytes.length} bytes)\\n`,\n );\n } else {\n process.stdout.write(\n ` ${fmt.dim('· No new attestation available yet, leaving file untouched.')}\\n`,\n );\n }\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { parse } from '../attestation/serialize.js';\nimport { verifyAttestation } from '../attestation/verify.js';\nimport { verifyOts } from '../ots/verify.js';\nimport { checkmark, crossmark, fmt } from './io.js';\n\nexport type VerifyArgs = {\n /** A URL to an article, or a path to a local markdown file. */\n target: string;\n /** Skip OTS Bitcoin verification (signature-only). */\n skipOts?: boolean;\n};\n\ntype Sources = {\n content: string;\n attestationJson: string;\n otsBytes: null | Uint8Array;\n};\n\nexport async function runVerify(args: VerifyArgs): Promise<boolean> {\n const sources = await loadSources(args);\n\n const attestation = parse(sources.attestationJson);\n const sigResult = await verifyAttestation({ attestation, content: sources.content });\n\n if (!sigResult.ok) {\n printSignatureFailure(sigResult.error);\n return false;\n }\n\n process.stdout.write(`${checkmark()} Content digest matches signature\\n`);\n process.stdout.write(\n `${checkmark()} Signature valid — signed by ${fmt.bold(sigResult.signerAddress)}\\n`,\n );\n\n if (args.skipOts || sources.otsBytes === null) {\n process.stdout.write(`${fmt.dim('· OTS verification skipped')}\\n`);\n return true;\n }\n\n const digest = hexToBytes(attestation.subject.contentDigest);\n const otsResult = await verifyOts(digest, sources.otsBytes);\n\n if (!otsResult.ok) {\n process.stdout.write(`${crossmark()} OTS: ${otsResult.reason}`);\n if (otsResult.details) {\n process.stdout.write(` — ${otsResult.details}`);\n }\n process.stdout.write(`\\n`);\n return otsResult.reason === 'pending-bitcoin'; // Pending is not a hard fail\n }\n\n process.stdout.write(\n `${checkmark()} Bitcoin timestamp confirmed at ${otsResult.bitcoinBlockTime.toISOString()}\\n`,\n );\n return true;\n}\n\nasync function loadSources(args: VerifyArgs): Promise<Sources> {\n if (/^https?:\\/\\//.test(args.target)) {\n return loadFromUrl(args.target);\n }\n return loadFromFile(args.target);\n}\n\nasync function loadFromFile(filePath: string): Promise<Sources> {\n const content = await readFile(filePath, 'utf8');\n const dir = dirname(filePath);\n const base = basename(filePath, extname(filePath));\n const attestationJson = await readFile(join(dir, `${base}.attestation.json`), 'utf8');\n let otsBytes: null | Uint8Array = null;\n try {\n const buf = await readFile(join(dir, `${base}.ots`));\n otsBytes = new Uint8Array(buf);\n } catch {\n otsBytes = null;\n }\n return { attestationJson, content, otsBytes };\n}\n\nasync function loadFromUrl(url: string): Promise<Sources> {\n const proofManifestUrl = await resolveManifestUrl(url);\n const manifest = await fetchJson<ProofManifest>(proofManifestUrl);\n\n const base = new URL(proofManifestUrl);\n const [content, attestationJson, otsBuf] = await Promise.all([\n fetchText(new URL(manifest.content, base).toString()),\n fetchText(new URL(manifest.attestation, base).toString()),\n fetchBytes(new URL(manifest.ots, base).toString()).catch(() => null),\n ]);\n return { attestationJson, content, otsBytes: otsBuf };\n}\n\ntype ProofManifest = {\n schemaVersion: number;\n slug: string;\n content: string;\n attestation: string;\n ots: string;\n};\n\nasync function resolveManifestUrl(articleUrl: string): Promise<string> {\n // Convention: append /proof.json to the article URL (trailing slash tolerated).\n const url = new URL(articleUrl);\n if (!url.pathname.endsWith('/')) {\n url.pathname += '/';\n }\n url.pathname += 'proof.json';\n return url.toString();\n}\n\nasync function fetchJson<T>(url: string): Promise<T> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return (await res.json()) as T;\n}\n\nasync function fetchText(url: string): Promise<string> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return res.text();\n}\n\nasync function fetchBytes(url: string): Promise<Uint8Array> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return new Uint8Array(await res.arrayBuffer());\n}\n\nfunction printSignatureFailure(error: Record<string, unknown> & { kind: string }): void {\n process.stdout.write(`${crossmark()} ${fmt.fail('Verification failed')}: ${error.kind}\\n`);\n for (const [k, v] of Object.entries(error)) {\n if (k === 'kind') {\n continue;\n }\n process.stdout.write(` ${k}: ${String(v)}\\n`);\n }\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { parseArgs } from 'node:util';\n\nimport { fmt } from './io.js';\nimport { runSign } from './sign.js';\nimport { runUpgrade } from './upgrade.js';\nimport { runVerify } from './verify.js';\n\nconst HELP = `${fmt.bold('attestation')} — sign articles with EIP-712 + Bitcoin (OpenTimestamps).\n\nUsage:\n attestation sign <file> --title <t> --slug <s> [--published-at <iso>]\n attestation verify <url|file> [--skip-ots]\n attestation upgrade <ots-file> [<ots-file> …]\n\nRun \\`attestation <command> --help\\` for command-specific options.\n`;\n\nexport async function runCli(argv: string[]): Promise<number> {\n const [command, ...rest] = argv;\n\n if (command === undefined || command === '-h' || command === '--help' || command === 'help') {\n process.stdout.write(HELP);\n return 0;\n }\n\n try {\n if (command === 'sign') {\n return await dispatchSign(rest);\n }\n if (command === 'verify') {\n return await dispatchVerify(rest);\n }\n if (command === 'upgrade') {\n return await dispatchUpgrade(rest);\n }\n process.stderr.write(`Unknown command: ${command}\\n${HELP}`);\n return 2;\n } catch (error) {\n process.stderr.write(`${fmt.fail('Error:')} ${(error as Error).message}\\n`);\n return 1;\n }\n}\n\nasync function dispatchSign(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n locale: { type: 'string' },\n 'prior-attestation': { type: 'string' },\n 'published-at': { type: 'string' },\n revision: { type: 'string' },\n 'skip-audit': { type: 'boolean' },\n 'skip-stamp': { type: 'boolean' },\n slug: { type: 'string' },\n title: { type: 'string' },\n },\n });\n\n const file = positionals[0];\n if (!file) {\n throw new Error('Missing required <file> argument.');\n }\n if (!values.title) {\n throw new Error('Missing required --title.');\n }\n if (!values.slug) {\n throw new Error('Missing required --slug.');\n }\n\n await runSign({\n file,\n locale: values.locale,\n priorAttestation: values['prior-attestation'] as `0x${string}` | undefined,\n publishedAt: values['published-at'],\n revision: values.revision ? Number(values.revision) : undefined,\n skipAudit: values['skip-audit'],\n skipStamp: values['skip-stamp'],\n slug: values.slug,\n title: values.title,\n });\n return 0;\n}\n\nasync function dispatchVerify(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n 'skip-ots': { type: 'boolean' },\n },\n });\n\n const target = positionals[0];\n if (!target) {\n throw new Error('Missing required <url|file> argument.');\n }\n\n const ok = await runVerify({ skipOts: values['skip-ots'], target });\n return ok ? 0 : 1;\n}\n\nasync function dispatchUpgrade(argv: string[]): Promise<number> {\n const { positionals } = parseArgs({ allowPositionals: true, args: argv, options: {} });\n await runUpgrade({ files: positionals });\n return 0;\n}\n","import { runCli } from './cli/index.js';\n\nvoid runCli(process.argv.slice(2)).then((code) => process.exit(code));\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -62,7 +62,8 @@ async function runSign(args) {
62
62
  process.stdout.write(`${checkmark()} Wrote ${attestationPath}\n`);
63
63
  if (!args.skipStamp) {
64
64
  process.stdout.write(`${fmt.info("→")} Submitting to OpenTimestamps calendars…\n`);
65
- const otsBytes = await stampDigest(hexToBytes$1(message.subject.contentDigest));
65
+ const digest = hexToBytes$1(message.subject.contentDigest);
66
+ const otsBytes = await stampDigest(digest);
66
67
  const otsPath = join(dir, `${baseName}.ots`);
67
68
  await writeFile(otsPath, Buffer.from(otsBytes));
68
69
  process.stdout.write(`${checkmark()} Wrote ${otsPath} (${otsBytes.length} bytes, calendar-only)\n`);
@@ -113,7 +114,8 @@ async function runVerify(args) {
113
114
  process.stdout.write(`${fmt.dim("· OTS verification skipped")}\n`);
114
115
  return true;
115
116
  }
116
- const otsResult = await verifyOts(hexToBytes(attestation.subject.contentDigest), sources.otsBytes);
117
+ const digest = hexToBytes(attestation.subject.contentDigest);
118
+ const otsResult = await verifyOts(digest, sources.otsBytes);
117
119
  if (!otsResult.ok) {
118
120
  process.stdout.write(`${crossmark()} OTS: ${otsResult.reason}`);
119
121
  if (otsResult.details) process.stdout.write(` — ${otsResult.details}`);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":["hexToBytes"],"sources":["../src/cli/io.ts","../src/cli/sign.ts","../src/cli/upgrade.ts","../src/cli/verify.ts","../src/cli/index.ts","../src/cli.ts"],"sourcesContent":["/* ANSI helpers — kept tiny so we don't pull in chalk. */\n\nconst supportsColor = process.stdout.isTTY === true && !process.env.NO_COLOR;\n\nexport const fmt = {\n bold: (s: string) => (supportsColor ? `\\x1b[1m${s}\\x1b[0m` : s),\n dim: (s: string) => (supportsColor ? `\\x1b[2m${s}\\x1b[0m` : s),\n fail: (s: string) => (supportsColor ? `\\x1b[31m${s}\\x1b[0m` : s),\n info: (s: string) => (supportsColor ? `\\x1b[36m${s}\\x1b[0m` : s),\n ok: (s: string) => (supportsColor ? `\\x1b[32m${s}\\x1b[0m` : s),\n warn: (s: string) => (supportsColor ? `\\x1b[33m${s}\\x1b[0m` : s),\n};\n\nexport function checkmark(): string {\n return fmt.ok('✓');\n}\n\nexport function crossmark(): string {\n return fmt.fail('✗');\n}\n","import { readFile, writeFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { buildAttestationMessage } from '../attestation/create.js';\nimport { stringify } from '../attestation/serialize.js';\nimport { type SignedAttestation } from '../attestation/types.js';\nimport { audit } from '../core/audit.js';\nimport { canonicalize } from '../core/canonicalize.js';\nimport { signViaBrowser } from '../eth/sign-flow.js';\nimport { stampDigest } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type SignArgs = {\n file: string;\n title: string;\n slug: string;\n locale?: string;\n publishedAt?: string;\n revision?: number;\n priorAttestation?: `0x${string}`;\n skipStamp?: boolean;\n skipAudit?: boolean;\n};\n\nexport async function runSign(args: SignArgs): Promise<void> {\n const content = await readFile(args.file, 'utf8');\n const locale = args.locale ?? deriveLocale(args.file);\n const publishedAt = args.publishedAt ? new Date(args.publishedAt) : new Date();\n\n const canonical = canonicalize(content);\n process.stdout.write(`${checkmark()} Canonicalized ${canonical.length} bytes (${locale})\\n`);\n\n if (!args.skipAudit) {\n const findings = audit(canonical);\n if (findings.length > 0) {\n process.stdout.write(`${fmt.warn('!')} ${findings.length} suspicious char(s) found:\\n`);\n for (const f of findings) {\n process.stdout.write(\n ` line ${f.line}, col ${f.column}: U+${f.codepoint\n .toString(16)\n .toUpperCase()\n .padStart(4, '0')} (${f.name})\\n`,\n );\n }\n process.stdout.write(` Pass --skip-audit to sign anyway.\\n`);\n throw new Error('Audit failed: clean up suspicious characters first.');\n }\n }\n\n const message = buildAttestationMessage({\n content,\n locale,\n priorAttestation: args.priorAttestation,\n publishedAt,\n revision: args.revision,\n slug: args.slug,\n title: args.title,\n });\n\n process.stdout.write(`${fmt.info('→')} Content digest: ${message.subject.contentDigest}\\n`);\n process.stdout.write(`${fmt.info('→')} Opening browser to sign with your wallet…\\n`);\n\n const { signature, signerAddress } = await signViaBrowser({ message });\n const signed: SignedAttestation = { ...message, signature, signerAddress };\n\n process.stdout.write(`${checkmark()} Signed by ${fmt.bold(signerAddress)}\\n`);\n\n const baseName = basename(args.file, extname(args.file));\n const dir = dirname(args.file);\n const attestationPath = join(dir, `${baseName}.attestation.json`);\n await writeFile(attestationPath, stringify(signed), 'utf8');\n process.stdout.write(`${checkmark()} Wrote ${attestationPath}\\n`);\n\n if (!args.skipStamp) {\n process.stdout.write(`${fmt.info('→')} Submitting to OpenTimestamps calendars…\\n`);\n const digest = hexToBytes(message.subject.contentDigest);\n const otsBytes = await stampDigest(digest);\n const otsPath = join(dir, `${baseName}.ots`);\n await writeFile(otsPath, Buffer.from(otsBytes));\n process.stdout.write(\n `${checkmark()} Wrote ${otsPath} (${otsBytes.length} bytes, calendar-only)\\n`,\n );\n process.stdout.write(\n ` ${fmt.dim('Run `attestation upgrade` in ~24h to anchor in Bitcoin.')}\\n`,\n );\n }\n}\n\nfunction deriveLocale(file: string): string {\n const name = basename(file, extname(file));\n if (/^[a-z]{2}$/.test(name)) {\n return name;\n }\n throw new Error(\n `Cannot derive locale from filename \"${basename(file)}\". Pass --locale explicitly.`,\n );\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { readFile, writeFile } from 'node:fs/promises';\n\nimport { upgradeProof } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type UpgradeArgs = {\n files: string[];\n};\n\nexport async function runUpgrade(args: UpgradeArgs): Promise<void> {\n if (args.files.length === 0) {\n throw new Error('No .ots files provided.');\n }\n\n for (const path of args.files) {\n process.stdout.write(`${fmt.info('→')} ${path}\\n`);\n const before = await readFile(path);\n const { bytes, upgraded } = await upgradeProof(new Uint8Array(before));\n\n if (upgraded) {\n await writeFile(path, Buffer.from(bytes));\n process.stdout.write(\n ` ${checkmark()} Upgraded to Bitcoin attestation (${bytes.length} bytes)\\n`,\n );\n } else {\n process.stdout.write(\n ` ${fmt.dim('· No new attestation available yet, leaving file untouched.')}\\n`,\n );\n }\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { parse } from '../attestation/serialize.js';\nimport { verifyAttestation } from '../attestation/verify.js';\nimport { verifyOts } from '../ots/verify.js';\nimport { checkmark, crossmark, fmt } from './io.js';\n\nexport type VerifyArgs = {\n /** A URL to an article, or a path to a local markdown file. */\n target: string;\n /** Skip OTS Bitcoin verification (signature-only). */\n skipOts?: boolean;\n};\n\ntype Sources = {\n content: string;\n attestationJson: string;\n otsBytes: null | Uint8Array;\n};\n\nexport async function runVerify(args: VerifyArgs): Promise<boolean> {\n const sources = await loadSources(args);\n\n const attestation = parse(sources.attestationJson);\n const sigResult = await verifyAttestation({ attestation, content: sources.content });\n\n if (!sigResult.ok) {\n printSignatureFailure(sigResult.error);\n return false;\n }\n\n process.stdout.write(`${checkmark()} Content digest matches signature\\n`);\n process.stdout.write(\n `${checkmark()} Signature valid — signed by ${fmt.bold(sigResult.signerAddress)}\\n`,\n );\n\n if (args.skipOts || sources.otsBytes === null) {\n process.stdout.write(`${fmt.dim('· OTS verification skipped')}\\n`);\n return true;\n }\n\n const digest = hexToBytes(attestation.subject.contentDigest);\n const otsResult = await verifyOts(digest, sources.otsBytes);\n\n if (!otsResult.ok) {\n process.stdout.write(`${crossmark()} OTS: ${otsResult.reason}`);\n if (otsResult.details) {\n process.stdout.write(` — ${otsResult.details}`);\n }\n process.stdout.write(`\\n`);\n return otsResult.reason === 'pending-bitcoin'; // Pending is not a hard fail\n }\n\n process.stdout.write(\n `${checkmark()} Bitcoin timestamp confirmed at ${otsResult.bitcoinBlockTime.toISOString()}\\n`,\n );\n return true;\n}\n\nasync function loadSources(args: VerifyArgs): Promise<Sources> {\n if (/^https?:\\/\\//.test(args.target)) {\n return loadFromUrl(args.target);\n }\n return loadFromFile(args.target);\n}\n\nasync function loadFromFile(filePath: string): Promise<Sources> {\n const content = await readFile(filePath, 'utf8');\n const dir = dirname(filePath);\n const base = basename(filePath, extname(filePath));\n const attestationJson = await readFile(join(dir, `${base}.attestation.json`), 'utf8');\n let otsBytes: null | Uint8Array = null;\n try {\n const buf = await readFile(join(dir, `${base}.ots`));\n otsBytes = new Uint8Array(buf);\n } catch {\n otsBytes = null;\n }\n return { attestationJson, content, otsBytes };\n}\n\nasync function loadFromUrl(url: string): Promise<Sources> {\n const proofManifestUrl = await resolveManifestUrl(url);\n const manifest = await fetchJson<ProofManifest>(proofManifestUrl);\n\n const base = new URL(proofManifestUrl);\n const [content, attestationJson, otsBuf] = await Promise.all([\n fetchText(new URL(manifest.content, base).toString()),\n fetchText(new URL(manifest.attestation, base).toString()),\n fetchBytes(new URL(manifest.ots, base).toString()).catch(() => null),\n ]);\n return { attestationJson, content, otsBytes: otsBuf };\n}\n\ntype ProofManifest = {\n schemaVersion: number;\n slug: string;\n content: string;\n attestation: string;\n ots: string;\n};\n\nasync function resolveManifestUrl(articleUrl: string): Promise<string> {\n // Convention: append /proof.json to the article URL (trailing slash tolerated).\n const url = new URL(articleUrl);\n if (!url.pathname.endsWith('/')) {\n url.pathname += '/';\n }\n url.pathname += 'proof.json';\n return url.toString();\n}\n\nasync function fetchJson<T>(url: string): Promise<T> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return (await res.json()) as T;\n}\n\nasync function fetchText(url: string): Promise<string> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return res.text();\n}\n\nasync function fetchBytes(url: string): Promise<Uint8Array> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return new Uint8Array(await res.arrayBuffer());\n}\n\nfunction printSignatureFailure(error: Record<string, unknown> & { kind: string }): void {\n process.stdout.write(`${crossmark()} ${fmt.fail('Verification failed')}: ${error.kind}\\n`);\n for (const [k, v] of Object.entries(error)) {\n if (k === 'kind') {\n continue;\n }\n process.stdout.write(` ${k}: ${String(v)}\\n`);\n }\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { parseArgs } from 'node:util';\n\nimport { fmt } from './io.js';\nimport { runSign } from './sign.js';\nimport { runUpgrade } from './upgrade.js';\nimport { runVerify } from './verify.js';\n\nconst HELP = `${fmt.bold('attestation')} — sign articles with EIP-712 + Bitcoin (OpenTimestamps).\n\nUsage:\n attestation sign <file> --title <t> --slug <s> [--published-at <iso>]\n attestation verify <url|file> [--skip-ots]\n attestation upgrade <ots-file> [<ots-file> …]\n\nRun \\`attestation <command> --help\\` for command-specific options.\n`;\n\nexport async function runCli(argv: string[]): Promise<number> {\n const [command, ...rest] = argv;\n\n if (command === undefined || command === '-h' || command === '--help' || command === 'help') {\n process.stdout.write(HELP);\n return 0;\n }\n\n try {\n if (command === 'sign') {\n return await dispatchSign(rest);\n }\n if (command === 'verify') {\n return await dispatchVerify(rest);\n }\n if (command === 'upgrade') {\n return await dispatchUpgrade(rest);\n }\n process.stderr.write(`Unknown command: ${command}\\n${HELP}`);\n return 2;\n } catch (error) {\n process.stderr.write(`${fmt.fail('Error:')} ${(error as Error).message}\\n`);\n return 1;\n }\n}\n\nasync function dispatchSign(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n locale: { type: 'string' },\n 'prior-attestation': { type: 'string' },\n 'published-at': { type: 'string' },\n revision: { type: 'string' },\n 'skip-audit': { type: 'boolean' },\n 'skip-stamp': { type: 'boolean' },\n slug: { type: 'string' },\n title: { type: 'string' },\n },\n });\n\n const file = positionals[0];\n if (!file) {\n throw new Error('Missing required <file> argument.');\n }\n if (!values.title) {\n throw new Error('Missing required --title.');\n }\n if (!values.slug) {\n throw new Error('Missing required --slug.');\n }\n\n await runSign({\n file,\n locale: values.locale,\n priorAttestation: values['prior-attestation'] as `0x${string}` | undefined,\n publishedAt: values['published-at'],\n revision: values.revision ? Number(values.revision) : undefined,\n skipAudit: values['skip-audit'],\n skipStamp: values['skip-stamp'],\n slug: values.slug,\n title: values.title,\n });\n return 0;\n}\n\nasync function dispatchVerify(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n 'skip-ots': { type: 'boolean' },\n },\n });\n\n const target = positionals[0];\n if (!target) {\n throw new Error('Missing required <url|file> argument.');\n }\n\n const ok = await runVerify({ skipOts: values['skip-ots'], target });\n return ok ? 0 : 1;\n}\n\nasync function dispatchUpgrade(argv: string[]): Promise<number> {\n const { positionals } = parseArgs({ allowPositionals: true, args: argv, options: {} });\n await runUpgrade({ files: positionals });\n return 0;\n}\n","import { runCli } from './cli/index.js';\n\nvoid runCli(process.argv.slice(2)).then((code) => process.exit(code));\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":["hexToBytes"],"sources":["../src/cli/io.ts","../src/cli/sign.ts","../src/cli/upgrade.ts","../src/cli/verify.ts","../src/cli/index.ts","../src/cli.ts"],"sourcesContent":["/* ANSI helpers — kept tiny so we don't pull in chalk. */\n\nconst supportsColor = process.stdout.isTTY === true && !process.env.NO_COLOR;\n\nexport const fmt = {\n bold: (s: string) => (supportsColor ? `\\x1b[1m${s}\\x1b[0m` : s),\n dim: (s: string) => (supportsColor ? `\\x1b[2m${s}\\x1b[0m` : s),\n fail: (s: string) => (supportsColor ? `\\x1b[31m${s}\\x1b[0m` : s),\n info: (s: string) => (supportsColor ? `\\x1b[36m${s}\\x1b[0m` : s),\n ok: (s: string) => (supportsColor ? `\\x1b[32m${s}\\x1b[0m` : s),\n warn: (s: string) => (supportsColor ? `\\x1b[33m${s}\\x1b[0m` : s),\n};\n\nexport function checkmark(): string {\n return fmt.ok('✓');\n}\n\nexport function crossmark(): string {\n return fmt.fail('✗');\n}\n","import { readFile, writeFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { buildAttestationMessage } from '../attestation/create.js';\nimport { stringify } from '../attestation/serialize.js';\nimport { type SignedAttestation } from '../attestation/types.js';\nimport { audit } from '../core/audit.js';\nimport { canonicalize } from '../core/canonicalize.js';\nimport { signViaBrowser } from '../eth/sign-flow.js';\nimport { stampDigest } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type SignArgs = {\n file: string;\n title: string;\n slug: string;\n locale?: string;\n publishedAt?: string;\n revision?: number;\n priorAttestation?: `0x${string}`;\n skipStamp?: boolean;\n skipAudit?: boolean;\n};\n\nexport async function runSign(args: SignArgs): Promise<void> {\n const content = await readFile(args.file, 'utf8');\n const locale = args.locale ?? deriveLocale(args.file);\n const publishedAt = args.publishedAt ? new Date(args.publishedAt) : new Date();\n\n const canonical = canonicalize(content);\n process.stdout.write(`${checkmark()} Canonicalized ${canonical.length} bytes (${locale})\\n`);\n\n if (!args.skipAudit) {\n const findings = audit(canonical);\n if (findings.length > 0) {\n process.stdout.write(`${fmt.warn('!')} ${findings.length} suspicious char(s) found:\\n`);\n for (const f of findings) {\n process.stdout.write(\n ` line ${f.line}, col ${f.column}: U+${f.codepoint\n .toString(16)\n .toUpperCase()\n .padStart(4, '0')} (${f.name})\\n`,\n );\n }\n process.stdout.write(` Pass --skip-audit to sign anyway.\\n`);\n throw new Error('Audit failed: clean up suspicious characters first.');\n }\n }\n\n const message = buildAttestationMessage({\n content,\n locale,\n priorAttestation: args.priorAttestation,\n publishedAt,\n revision: args.revision,\n slug: args.slug,\n title: args.title,\n });\n\n process.stdout.write(`${fmt.info('→')} Content digest: ${message.subject.contentDigest}\\n`);\n process.stdout.write(`${fmt.info('→')} Opening browser to sign with your wallet…\\n`);\n\n const { signature, signerAddress } = await signViaBrowser({ message });\n const signed: SignedAttestation = { ...message, signature, signerAddress };\n\n process.stdout.write(`${checkmark()} Signed by ${fmt.bold(signerAddress)}\\n`);\n\n const baseName = basename(args.file, extname(args.file));\n const dir = dirname(args.file);\n const attestationPath = join(dir, `${baseName}.attestation.json`);\n await writeFile(attestationPath, stringify(signed), 'utf8');\n process.stdout.write(`${checkmark()} Wrote ${attestationPath}\\n`);\n\n if (!args.skipStamp) {\n process.stdout.write(`${fmt.info('→')} Submitting to OpenTimestamps calendars…\\n`);\n const digest = hexToBytes(message.subject.contentDigest);\n const otsBytes = await stampDigest(digest);\n const otsPath = join(dir, `${baseName}.ots`);\n await writeFile(otsPath, Buffer.from(otsBytes));\n process.stdout.write(\n `${checkmark()} Wrote ${otsPath} (${otsBytes.length} bytes, calendar-only)\\n`,\n );\n process.stdout.write(\n ` ${fmt.dim('Run `attestation upgrade` in ~24h to anchor in Bitcoin.')}\\n`,\n );\n }\n}\n\nfunction deriveLocale(file: string): string {\n const name = basename(file, extname(file));\n if (/^[a-z]{2}$/.test(name)) {\n return name;\n }\n throw new Error(\n `Cannot derive locale from filename \"${basename(file)}\". Pass --locale explicitly.`,\n );\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { readFile, writeFile } from 'node:fs/promises';\n\nimport { upgradeProof } from '../ots/stamp.js';\nimport { checkmark, fmt } from './io.js';\n\nexport type UpgradeArgs = {\n files: string[];\n};\n\nexport async function runUpgrade(args: UpgradeArgs): Promise<void> {\n if (args.files.length === 0) {\n throw new Error('No .ots files provided.');\n }\n\n for (const path of args.files) {\n process.stdout.write(`${fmt.info('→')} ${path}\\n`);\n const before = await readFile(path);\n const { bytes, upgraded } = await upgradeProof(new Uint8Array(before));\n\n if (upgraded) {\n await writeFile(path, Buffer.from(bytes));\n process.stdout.write(\n ` ${checkmark()} Upgraded to Bitcoin attestation (${bytes.length} bytes)\\n`,\n );\n } else {\n process.stdout.write(\n ` ${fmt.dim('· No new attestation available yet, leaving file untouched.')}\\n`,\n );\n }\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { parse } from '../attestation/serialize.js';\nimport { verifyAttestation } from '../attestation/verify.js';\nimport { verifyOts } from '../ots/verify.js';\nimport { checkmark, crossmark, fmt } from './io.js';\n\nexport type VerifyArgs = {\n /** A URL to an article, or a path to a local markdown file. */\n target: string;\n /** Skip OTS Bitcoin verification (signature-only). */\n skipOts?: boolean;\n};\n\ntype Sources = {\n content: string;\n attestationJson: string;\n otsBytes: null | Uint8Array;\n};\n\nexport async function runVerify(args: VerifyArgs): Promise<boolean> {\n const sources = await loadSources(args);\n\n const attestation = parse(sources.attestationJson);\n const sigResult = await verifyAttestation({ attestation, content: sources.content });\n\n if (!sigResult.ok) {\n printSignatureFailure(sigResult.error);\n return false;\n }\n\n process.stdout.write(`${checkmark()} Content digest matches signature\\n`);\n process.stdout.write(\n `${checkmark()} Signature valid — signed by ${fmt.bold(sigResult.signerAddress)}\\n`,\n );\n\n if (args.skipOts || sources.otsBytes === null) {\n process.stdout.write(`${fmt.dim('· OTS verification skipped')}\\n`);\n return true;\n }\n\n const digest = hexToBytes(attestation.subject.contentDigest);\n const otsResult = await verifyOts(digest, sources.otsBytes);\n\n if (!otsResult.ok) {\n process.stdout.write(`${crossmark()} OTS: ${otsResult.reason}`);\n if (otsResult.details) {\n process.stdout.write(` — ${otsResult.details}`);\n }\n process.stdout.write(`\\n`);\n return otsResult.reason === 'pending-bitcoin'; // Pending is not a hard fail\n }\n\n process.stdout.write(\n `${checkmark()} Bitcoin timestamp confirmed at ${otsResult.bitcoinBlockTime.toISOString()}\\n`,\n );\n return true;\n}\n\nasync function loadSources(args: VerifyArgs): Promise<Sources> {\n if (/^https?:\\/\\//.test(args.target)) {\n return loadFromUrl(args.target);\n }\n return loadFromFile(args.target);\n}\n\nasync function loadFromFile(filePath: string): Promise<Sources> {\n const content = await readFile(filePath, 'utf8');\n const dir = dirname(filePath);\n const base = basename(filePath, extname(filePath));\n const attestationJson = await readFile(join(dir, `${base}.attestation.json`), 'utf8');\n let otsBytes: null | Uint8Array = null;\n try {\n const buf = await readFile(join(dir, `${base}.ots`));\n otsBytes = new Uint8Array(buf);\n } catch {\n otsBytes = null;\n }\n return { attestationJson, content, otsBytes };\n}\n\nasync function loadFromUrl(url: string): Promise<Sources> {\n const proofManifestUrl = await resolveManifestUrl(url);\n const manifest = await fetchJson<ProofManifest>(proofManifestUrl);\n\n const base = new URL(proofManifestUrl);\n const [content, attestationJson, otsBuf] = await Promise.all([\n fetchText(new URL(manifest.content, base).toString()),\n fetchText(new URL(manifest.attestation, base).toString()),\n fetchBytes(new URL(manifest.ots, base).toString()).catch(() => null),\n ]);\n return { attestationJson, content, otsBytes: otsBuf };\n}\n\ntype ProofManifest = {\n schemaVersion: number;\n slug: string;\n content: string;\n attestation: string;\n ots: string;\n};\n\nasync function resolveManifestUrl(articleUrl: string): Promise<string> {\n // Convention: append /proof.json to the article URL (trailing slash tolerated).\n const url = new URL(articleUrl);\n if (!url.pathname.endsWith('/')) {\n url.pathname += '/';\n }\n url.pathname += 'proof.json';\n return url.toString();\n}\n\nasync function fetchJson<T>(url: string): Promise<T> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return (await res.json()) as T;\n}\n\nasync function fetchText(url: string): Promise<string> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return res.text();\n}\n\nasync function fetchBytes(url: string): Promise<Uint8Array> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Fetch ${url} failed: HTTP ${res.status}`);\n }\n return new Uint8Array(await res.arrayBuffer());\n}\n\nfunction printSignatureFailure(error: Record<string, unknown> & { kind: string }): void {\n process.stdout.write(`${crossmark()} ${fmt.fail('Verification failed')}: ${error.kind}\\n`);\n for (const [k, v] of Object.entries(error)) {\n if (k === 'kind') {\n continue;\n }\n process.stdout.write(` ${k}: ${String(v)}\\n`);\n }\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const stripped = hex.slice(2);\n const out = new Uint8Array(stripped.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { parseArgs } from 'node:util';\n\nimport { fmt } from './io.js';\nimport { runSign } from './sign.js';\nimport { runUpgrade } from './upgrade.js';\nimport { runVerify } from './verify.js';\n\nconst HELP = `${fmt.bold('attestation')} — sign articles with EIP-712 + Bitcoin (OpenTimestamps).\n\nUsage:\n attestation sign <file> --title <t> --slug <s> [--published-at <iso>]\n attestation verify <url|file> [--skip-ots]\n attestation upgrade <ots-file> [<ots-file> …]\n\nRun \\`attestation <command> --help\\` for command-specific options.\n`;\n\nexport async function runCli(argv: string[]): Promise<number> {\n const [command, ...rest] = argv;\n\n if (command === undefined || command === '-h' || command === '--help' || command === 'help') {\n process.stdout.write(HELP);\n return 0;\n }\n\n try {\n if (command === 'sign') {\n return await dispatchSign(rest);\n }\n if (command === 'verify') {\n return await dispatchVerify(rest);\n }\n if (command === 'upgrade') {\n return await dispatchUpgrade(rest);\n }\n process.stderr.write(`Unknown command: ${command}\\n${HELP}`);\n return 2;\n } catch (error) {\n process.stderr.write(`${fmt.fail('Error:')} ${(error as Error).message}\\n`);\n return 1;\n }\n}\n\nasync function dispatchSign(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n locale: { type: 'string' },\n 'prior-attestation': { type: 'string' },\n 'published-at': { type: 'string' },\n revision: { type: 'string' },\n 'skip-audit': { type: 'boolean' },\n 'skip-stamp': { type: 'boolean' },\n slug: { type: 'string' },\n title: { type: 'string' },\n },\n });\n\n const file = positionals[0];\n if (!file) {\n throw new Error('Missing required <file> argument.');\n }\n if (!values.title) {\n throw new Error('Missing required --title.');\n }\n if (!values.slug) {\n throw new Error('Missing required --slug.');\n }\n\n await runSign({\n file,\n locale: values.locale,\n priorAttestation: values['prior-attestation'] as `0x${string}` | undefined,\n publishedAt: values['published-at'],\n revision: values.revision ? Number(values.revision) : undefined,\n skipAudit: values['skip-audit'],\n skipStamp: values['skip-stamp'],\n slug: values.slug,\n title: values.title,\n });\n return 0;\n}\n\nasync function dispatchVerify(argv: string[]): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n args: argv,\n options: {\n 'skip-ots': { type: 'boolean' },\n },\n });\n\n const target = positionals[0];\n if (!target) {\n throw new Error('Missing required <url|file> argument.');\n }\n\n const ok = await runVerify({ skipOts: values['skip-ots'], target });\n return ok ? 0 : 1;\n}\n\nasync function dispatchUpgrade(argv: string[]): Promise<number> {\n const { positionals } = parseArgs({ allowPositionals: true, args: argv, options: {} });\n await runUpgrade({ files: positionals });\n return 0;\n}\n","import { runCli } from './cli/index.js';\n\nvoid runCli(process.argv.slice(2)).then((code) => process.exit(code));\n"],"mappings":"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"}
package/dist/node.cjs CHANGED
@@ -147,7 +147,7 @@ function buildBatchSignPageHtml(entries) {
147
147
  }
148
148
  //#endregion
149
149
  //#region src/eth/sign-batch-flow.ts
150
- const DEFAULT_TIMEOUT_MS = 1800 * 1e3;
150
+ const DEFAULT_TIMEOUT_MS = 18e5;
151
151
  async function signBatchViaBrowser(opts) {
152
152
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
153
153
  const html = buildBatchSignPageHtml(opts.entries.map((e) => ({
package/dist/node.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.cjs","names":["ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1"],"sources":["../src/eth/sign-batch-page-html.ts","../src/eth/sign-batch-flow.ts"],"sourcesContent":["/**\n * HTML page served by the local CLI server during batch signing. Loads the\n * full list of EIP-712 typed-data messages, walks them sequentially, and POSTs\n * each signature back as MetaMask returns it. The server closes after the user\n * either signs the last entry or clicks \"Done\".\n */\nexport function buildBatchSignPageHtml(\n entries: Array<{ id: string; label: string; typedData: object }>,\n): string {\n const escaped = JSON.stringify(entries)\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign all attestations</title>\n<style>\n * { box-sizing: border-box; }\n body { font-family: system-ui, sans-serif; max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.5; color: #111; }\n h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }\n .lead { color: #666; margin: 0 0 2rem; }\n .controls { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; align-items: center; }\n button { padding: 0.6rem 1rem; font-size: 0.95rem; cursor: pointer; border: 1px solid #ccc; background: #fff; border-radius: 6px; }\n button.primary { background: #111; color: #fff; border-color: #111; }\n button:disabled { opacity: 0.5; cursor: not-allowed; }\n .progress { font-size: 0.85rem; color: #666; }\n ul.entries { list-style: none; padding: 0; margin: 0; border-top: 1px solid #eee; }\n ul.entries li { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid #eee; font-size: 0.9rem; }\n .pill { font-family: ui-monospace, monospace; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; padding: 2px 8px; border-radius: 999px; border: 1px solid; }\n .pill.idle { color: #999; border-color: #ddd; }\n .pill.active { color: #0070f3; border-color: #c6dfff; background: #f0f7ff; }\n .pill.signed { color: #0a7d22; border-color: #b8e6c1; background: #f0faf3; }\n .pill.skipped { color: #b78403; border-color: #f1d57f; background: #fffaeb; }\n .pill.error { color: #b00020; border-color: #f5b8c1; background: #fef0f2; }\n .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .err-msg { font-size: 0.75rem; color: #b00020; padding: 0 0 0.5rem 0; }\n pre.preview { max-height: 200px; overflow: auto; font-size: 0.8rem; background: #f7f7f7; padding: 0.75rem; border-radius: 6px; margin: 0 0 1.5rem; }\n</style>\n</head>\n<body>\n<h1>Sign attestations</h1>\n<p class=\"lead\" id=\"lead\"></p>\n<div class=\"controls\">\n <button class=\"primary\" id=\"run\">Sign all</button>\n <button id=\"finish\" disabled>Done</button>\n <span class=\"progress\" id=\"progress\"></span>\n</div>\n<ul class=\"entries\" id=\"entries\"></ul>\n<script>\n const ENTRIES = JSON.parse(\\`${escaped}\\`);\n const ul = document.getElementById('entries');\n const lead = document.getElementById('lead');\n const progress = document.getElementById('progress');\n const runBtn = document.getElementById('run');\n const finishBtn = document.getElementById('finish');\n\n lead.textContent = ENTRIES.length + ' article' + (ENTRIES.length === 1 ? '' : 's') + ' to sign. MetaMask will pop up once per article — review the typed data and confirm.';\n progress.textContent = '0 / ' + ENTRIES.length;\n\n const rows = ENTRIES.map((e, i) => {\n const li = document.createElement('li');\n const label = document.createElement('span');\n label.className = 'label';\n label.textContent = (i + 1) + '. ' + e.label;\n const pill = document.createElement('span');\n pill.className = 'pill idle';\n pill.textContent = 'idle';\n li.appendChild(pill);\n li.appendChild(label);\n ul.appendChild(li);\n return { pill, li };\n });\n\n let signed = 0;\n\n async function signOne(i) {\n const entry = ENTRIES[i];\n rows[i].pill.className = 'pill active';\n rows[i].pill.textContent = 'signing';\n\n if (!window.ethereum) throw new Error('No window.ethereum provider');\n\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n let signature;\n try {\n signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(entry.typedData)],\n });\n } catch (err) {\n const msg = err && err.message ? err.message : String(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'rejected';\n const errLi = document.createElement('div');\n errLi.className = 'err-msg';\n errLi.textContent = msg;\n rows[i].li.appendChild(errLi);\n await fetch('/skip', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, reason: msg }),\n });\n return false;\n }\n\n const res = await fetch('/sig', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, signature, signerAddress: signer }),\n });\n if (!res.ok) {\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'server-rejected';\n return false;\n }\n rows[i].pill.className = 'pill signed';\n rows[i].pill.textContent = 'signed';\n signed++;\n progress.textContent = signed + ' / ' + ENTRIES.length;\n return true;\n }\n\n runBtn.addEventListener('click', async () => {\n runBtn.disabled = true;\n for (let i = 0; i < ENTRIES.length; i++) {\n try { await signOne(i); } catch (err) {\n console.error(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'error';\n }\n }\n finishBtn.disabled = false;\n finishBtn.classList.add('primary');\n });\n\n finishBtn.addEventListener('click', async () => {\n finishBtn.disabled = true;\n await fetch('/done', { method: 'POST' });\n document.body.innerHTML = '<h1>Done.</h1><p>You can close this tab.</p>';\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer } from 'node:http';\nimport { type AddressInfo } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildBatchSignPageHtml } from './sign-batch-page-html.js';\n\nexport type BatchSignEntry = {\n /** Unique key — used to correlate signatures back to entries. */\n id: string;\n /** Human-readable label shown in the browser UI. */\n label: string;\n /** EIP-712 message to sign. */\n message: AttestationMessage;\n};\n\nexport type BatchSignFlowOptions = {\n entries: BatchSignEntry[];\n onUrlReady?: (url: string) => Promise<void> | void;\n /** Total session timeout. Default 30 minutes. */\n timeoutMs?: number;\n};\n\nexport type BatchSignature = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nexport type BatchSignFlowResult = {\n signatures: Map<string, BatchSignature>;\n /** Entries the user explicitly skipped or that failed (rejected wallet, etc.). */\n skipped: Map<string, string>;\n};\n\nconst DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;\n\nexport async function signBatchViaBrowser(\n opts: BatchSignFlowOptions,\n): Promise<BatchSignFlowResult> {\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const wireEntries = opts.entries.map((e) => ({\n id: e.id,\n label: e.label,\n typedData: {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(e.message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n },\n }));\n const html = buildBatchSignPageHtml(wireEntries);\n\n const signatures = new Map<string, BatchSignature>();\n const skipped = new Map<string, string>();\n\n return new Promise<BatchSignFlowResult>((resolve, reject) => {\n const server = createServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && (req.url === '/sig' || req.url === '/skip')) {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: {\n id: string;\n signature?: `0x${string}`;\n signerAddress?: `0x${string}`;\n reason?: string;\n };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return;\n }\n if (req.url === '/sig') {\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return;\n }\n signatures.set(parsed.id, {\n signature: parsed.signature,\n signerAddress: parsed.signerAddress,\n });\n } else {\n skipped.set(parsed.id, parsed.reason ?? 'skipped');\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n });\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve({ signatures, skipped });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Batch sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.cjs","names":["ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1","createServer"],"sources":["../src/eth/sign-batch-page-html.ts","../src/eth/sign-batch-flow.ts"],"sourcesContent":["/**\n * HTML page served by the local CLI server during batch signing. Loads the\n * full list of EIP-712 typed-data messages, walks them sequentially, and POSTs\n * each signature back as MetaMask returns it. The server closes after the user\n * either signs the last entry or clicks \"Done\".\n */\nexport function buildBatchSignPageHtml(\n entries: Array<{ id: string; label: string; typedData: object }>,\n): string {\n const escaped = JSON.stringify(entries)\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign all attestations</title>\n<style>\n * { box-sizing: border-box; }\n body { font-family: system-ui, sans-serif; max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.5; color: #111; }\n h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }\n .lead { color: #666; margin: 0 0 2rem; }\n .controls { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; align-items: center; }\n button { padding: 0.6rem 1rem; font-size: 0.95rem; cursor: pointer; border: 1px solid #ccc; background: #fff; border-radius: 6px; }\n button.primary { background: #111; color: #fff; border-color: #111; }\n button:disabled { opacity: 0.5; cursor: not-allowed; }\n .progress { font-size: 0.85rem; color: #666; }\n ul.entries { list-style: none; padding: 0; margin: 0; border-top: 1px solid #eee; }\n ul.entries li { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid #eee; font-size: 0.9rem; }\n .pill { font-family: ui-monospace, monospace; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; padding: 2px 8px; border-radius: 999px; border: 1px solid; }\n .pill.idle { color: #999; border-color: #ddd; }\n .pill.active { color: #0070f3; border-color: #c6dfff; background: #f0f7ff; }\n .pill.signed { color: #0a7d22; border-color: #b8e6c1; background: #f0faf3; }\n .pill.skipped { color: #b78403; border-color: #f1d57f; background: #fffaeb; }\n .pill.error { color: #b00020; border-color: #f5b8c1; background: #fef0f2; }\n .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .err-msg { font-size: 0.75rem; color: #b00020; padding: 0 0 0.5rem 0; }\n pre.preview { max-height: 200px; overflow: auto; font-size: 0.8rem; background: #f7f7f7; padding: 0.75rem; border-radius: 6px; margin: 0 0 1.5rem; }\n</style>\n</head>\n<body>\n<h1>Sign attestations</h1>\n<p class=\"lead\" id=\"lead\"></p>\n<div class=\"controls\">\n <button class=\"primary\" id=\"run\">Sign all</button>\n <button id=\"finish\" disabled>Done</button>\n <span class=\"progress\" id=\"progress\"></span>\n</div>\n<ul class=\"entries\" id=\"entries\"></ul>\n<script>\n const ENTRIES = JSON.parse(\\`${escaped}\\`);\n const ul = document.getElementById('entries');\n const lead = document.getElementById('lead');\n const progress = document.getElementById('progress');\n const runBtn = document.getElementById('run');\n const finishBtn = document.getElementById('finish');\n\n lead.textContent = ENTRIES.length + ' article' + (ENTRIES.length === 1 ? '' : 's') + ' to sign. MetaMask will pop up once per article — review the typed data and confirm.';\n progress.textContent = '0 / ' + ENTRIES.length;\n\n const rows = ENTRIES.map((e, i) => {\n const li = document.createElement('li');\n const label = document.createElement('span');\n label.className = 'label';\n label.textContent = (i + 1) + '. ' + e.label;\n const pill = document.createElement('span');\n pill.className = 'pill idle';\n pill.textContent = 'idle';\n li.appendChild(pill);\n li.appendChild(label);\n ul.appendChild(li);\n return { pill, li };\n });\n\n let signed = 0;\n\n async function signOne(i) {\n const entry = ENTRIES[i];\n rows[i].pill.className = 'pill active';\n rows[i].pill.textContent = 'signing';\n\n if (!window.ethereum) throw new Error('No window.ethereum provider');\n\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n let signature;\n try {\n signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(entry.typedData)],\n });\n } catch (err) {\n const msg = err && err.message ? err.message : String(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'rejected';\n const errLi = document.createElement('div');\n errLi.className = 'err-msg';\n errLi.textContent = msg;\n rows[i].li.appendChild(errLi);\n await fetch('/skip', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, reason: msg }),\n });\n return false;\n }\n\n const res = await fetch('/sig', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, signature, signerAddress: signer }),\n });\n if (!res.ok) {\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'server-rejected';\n return false;\n }\n rows[i].pill.className = 'pill signed';\n rows[i].pill.textContent = 'signed';\n signed++;\n progress.textContent = signed + ' / ' + ENTRIES.length;\n return true;\n }\n\n runBtn.addEventListener('click', async () => {\n runBtn.disabled = true;\n for (let i = 0; i < ENTRIES.length; i++) {\n try { await signOne(i); } catch (err) {\n console.error(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'error';\n }\n }\n finishBtn.disabled = false;\n finishBtn.classList.add('primary');\n });\n\n finishBtn.addEventListener('click', async () => {\n finishBtn.disabled = true;\n await fetch('/done', { method: 'POST' });\n document.body.innerHTML = '<h1>Done.</h1><p>You can close this tab.</p>';\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer } from 'node:http';\nimport { type AddressInfo } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildBatchSignPageHtml } from './sign-batch-page-html.js';\n\nexport type BatchSignEntry = {\n /** Unique key — used to correlate signatures back to entries. */\n id: string;\n /** Human-readable label shown in the browser UI. */\n label: string;\n /** EIP-712 message to sign. */\n message: AttestationMessage;\n};\n\nexport type BatchSignFlowOptions = {\n entries: BatchSignEntry[];\n onUrlReady?: (url: string) => Promise<void> | void;\n /** Total session timeout. Default 30 minutes. */\n timeoutMs?: number;\n};\n\nexport type BatchSignature = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nexport type BatchSignFlowResult = {\n signatures: Map<string, BatchSignature>;\n /** Entries the user explicitly skipped or that failed (rejected wallet, etc.). */\n skipped: Map<string, string>;\n};\n\nconst DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;\n\nexport async function signBatchViaBrowser(\n opts: BatchSignFlowOptions,\n): Promise<BatchSignFlowResult> {\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const wireEntries = opts.entries.map((e) => ({\n id: e.id,\n label: e.label,\n typedData: {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(e.message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n },\n }));\n const html = buildBatchSignPageHtml(wireEntries);\n\n const signatures = new Map<string, BatchSignature>();\n const skipped = new Map<string, string>();\n\n return new Promise<BatchSignFlowResult>((resolve, reject) => {\n const server = createServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && (req.url === '/sig' || req.url === '/skip')) {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: {\n id: string;\n signature?: `0x${string}`;\n signerAddress?: `0x${string}`;\n reason?: string;\n };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return;\n }\n if (req.url === '/sig') {\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return;\n }\n signatures.set(parsed.id, {\n signature: parsed.signature,\n signerAddress: parsed.signerAddress,\n });\n } else {\n skipped.set(parsed.id, parsed.reason ?? 'skipped');\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n });\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve({ signatures, skipped });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Batch sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n"],"mappings":";;;;;;;;;;;AAMA,SAAgB,uBACZ,SACM;CAMN,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCALS,KAAK,UAAU,OAAO,CAAC,CAClC,QAAQ,OAAO,OAAO,GAAG,IAAI,CAAC,CAC9B,QAAQ,MAAM,KAAK,CAAC,CACpB,QAAQ,gBAAgB,OAAO,GAAG,YAwCF,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgG3C;;;AC9GA,MAAM,qBAAqB;AAE3B,eAAsB,oBAClB,MAC4B;CAC5B,MAAM,YAAY,KAAK,aAAa;CAYpC,MAAM,OAAO,uBAVO,KAAK,QAAQ,KAAK,OAAO;EACzC,IAAI,EAAE;EACN,OAAO,EAAE;EACT,WAAW;GACP,QAAQA,sBAAAA;GACR,SAAS,2BAA2B,EAAE,OAAO;GAC7C,aAAaC,sBAAAA;GACb,OAAO;IAAE,cAAc,YAAY;IAAG,GAAGC,sBAAAA;GAAqB;EAClE;CACJ,EACoC,CAAW;CAE/C,MAAM,6BAAa,IAAI,IAA4B;CACnD,MAAM,0BAAU,IAAI,IAAoB;CAExC,OAAO,IAAI,SAA8B,SAAS,WAAW;EACzD,MAAM,UAAA,GAASC,UAAAA,aAAAA,EAAc,KAAK,QAAQ;GACtC,IAAI,IAAI,WAAW,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,gBAAgB;IACxE,IAAI,UAAU,KAAK,EAAE,gBAAgB,2BAA2B,CAAC;IACjE,IAAI,IAAI,IAAI;IACZ;GACJ;GAEA,IAAI,IAAI,WAAW,WAAW,IAAI,QAAQ,UAAU,IAAI,QAAQ,UAAU;IACtE,IAAI,OAAO;IACX,IAAI,GAAG,SAAS,UAAkB;KAC9B,QAAQ,MAAM,SAAS,MAAM;IACjC,CAAC;IACD,IAAI,GAAG,aAAa;KAChB,IAAI;KAMJ,IAAI;MACA,SAAS,KAAK,MAAM,IAAI;KAC5B,QAAQ;MACJ,IAAI,UAAU,GAAG;MACjB,IAAI,IAAI,aAAa;MACrB;KACJ;KACA,IAAI,IAAI,QAAQ,QAAQ;MACpB,IACI,OAAO,OAAO,cAAc,YAC5B,CAAC,uBAAuB,KAAK,OAAO,SAAS,KAC7C,OAAO,OAAO,kBAAkB,YAChC,CAAC,sBAAsB,KAAK,OAAO,aAAa,GAClD;OACE,IAAI,UAAU,GAAG;OACjB,IAAI,IAAI,iBAAiB;OACzB;MACJ;MACA,WAAW,IAAI,OAAO,IAAI;OACtB,WAAW,OAAO;OAClB,eAAe,OAAO;MAC1B,CAAC;KACL,OACI,QAAQ,IAAI,OAAO,IAAI,OAAO,UAAU,SAAS;KAErD,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;KACnD,IAAI,IAAI,IAAI;IAChB,CAAC;IACD;GACJ;GAEA,IAAI,IAAI,WAAW,UAAU,IAAI,QAAQ,SAAS;IAC9C,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;IACnD,IAAI,IAAI,IAAI;IACZ,IAAI,GAAG,gBAAgB;KACnB,aAAa,KAAK;KAClB,OAAO,MAAM;KACb,QAAQ;MAAE;MAAY;KAAQ,CAAC;IACnC,CAAC;IACD;GACJ;GAEA,IAAI,UAAU,GAAG;GACjB,IAAI,IAAI,WAAW;EACvB,CAAC;EAED,MAAM,QAAQ,iBAAiB;GAC3B,OAAO,MAAM;GACb,uBAAO,IAAI,MAAM,mCAAmC,UAAU,GAAG,CAAC;EACtE,GAAG,SAAS;EAEZ,OAAO,GAAG,UAAU,QAAQ;GACxB,aAAa,KAAK;GAClB,OAAO,GAAG;EACd,CAAC;EAED,OAAO,OAAO,GAAG,mBAAmB;GAEhC,MAAM,MAAM,oBADC,OAAO,QACe,CAAC,CAAC,KAAK;GAC1C,MAAM,QAAQ,KAAK,cAAc;GACjC,QAAa,QAAQ,MAAM,GAAG,CAAC,CAAC,CAAC,OAAO,UAAmB;IACvD,aAAa,KAAK;IAClB,OAAO,MAAM;IACb,OAAO,KAAK;GAChB,CAAC;EACL,CAAC;CACL,CAAC;AACL;AAEA,eAAe,qBAAqB,KAA4B;CAC5D,MAAM,EAAE,SAAS,SAAS,MAAM,OAAO;CACvC,MAAM,KAAK,GAAG;AAClB;AAEA,SAAS,2BAA2B,SAAsD;CACtF,OAAO;EACH,QAAQ;GACJ,kBAAkB,QAAQ,OAAO;GACjC,aAAa,QAAQ,OAAO,YAAY,SAAS;GACjD,UAAU,QAAQ,OAAO;GACzB,MAAM,QAAQ,OAAO;EACzB;EACA,eAAe,QAAQ;EACvB,SAAS,QAAQ;CACrB;AACJ;AAEA,SAAS,cAAqD;CAC1D,OAAO;EACH;GAAE,MAAM;GAAQ,MAAM;EAAS;EAC/B;GAAE,MAAM;GAAW,MAAM;EAAS;EAClC;GAAE,MAAM;GAAW,MAAM;EAAU;CACvC;AACJ"}
package/dist/node.js CHANGED
@@ -146,7 +146,7 @@ function buildBatchSignPageHtml(entries) {
146
146
  }
147
147
  //#endregion
148
148
  //#region src/eth/sign-batch-flow.ts
149
- const DEFAULT_TIMEOUT_MS = 1800 * 1e3;
149
+ const DEFAULT_TIMEOUT_MS = 18e5;
150
150
  async function signBatchViaBrowser(opts) {
151
151
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
152
152
  const html = buildBatchSignPageHtml(opts.entries.map((e) => ({
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.js","names":[],"sources":["../src/eth/sign-batch-page-html.ts","../src/eth/sign-batch-flow.ts"],"sourcesContent":["/**\n * HTML page served by the local CLI server during batch signing. Loads the\n * full list of EIP-712 typed-data messages, walks them sequentially, and POSTs\n * each signature back as MetaMask returns it. The server closes after the user\n * either signs the last entry or clicks \"Done\".\n */\nexport function buildBatchSignPageHtml(\n entries: Array<{ id: string; label: string; typedData: object }>,\n): string {\n const escaped = JSON.stringify(entries)\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign all attestations</title>\n<style>\n * { box-sizing: border-box; }\n body { font-family: system-ui, sans-serif; max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.5; color: #111; }\n h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }\n .lead { color: #666; margin: 0 0 2rem; }\n .controls { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; align-items: center; }\n button { padding: 0.6rem 1rem; font-size: 0.95rem; cursor: pointer; border: 1px solid #ccc; background: #fff; border-radius: 6px; }\n button.primary { background: #111; color: #fff; border-color: #111; }\n button:disabled { opacity: 0.5; cursor: not-allowed; }\n .progress { font-size: 0.85rem; color: #666; }\n ul.entries { list-style: none; padding: 0; margin: 0; border-top: 1px solid #eee; }\n ul.entries li { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid #eee; font-size: 0.9rem; }\n .pill { font-family: ui-monospace, monospace; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; padding: 2px 8px; border-radius: 999px; border: 1px solid; }\n .pill.idle { color: #999; border-color: #ddd; }\n .pill.active { color: #0070f3; border-color: #c6dfff; background: #f0f7ff; }\n .pill.signed { color: #0a7d22; border-color: #b8e6c1; background: #f0faf3; }\n .pill.skipped { color: #b78403; border-color: #f1d57f; background: #fffaeb; }\n .pill.error { color: #b00020; border-color: #f5b8c1; background: #fef0f2; }\n .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .err-msg { font-size: 0.75rem; color: #b00020; padding: 0 0 0.5rem 0; }\n pre.preview { max-height: 200px; overflow: auto; font-size: 0.8rem; background: #f7f7f7; padding: 0.75rem; border-radius: 6px; margin: 0 0 1.5rem; }\n</style>\n</head>\n<body>\n<h1>Sign attestations</h1>\n<p class=\"lead\" id=\"lead\"></p>\n<div class=\"controls\">\n <button class=\"primary\" id=\"run\">Sign all</button>\n <button id=\"finish\" disabled>Done</button>\n <span class=\"progress\" id=\"progress\"></span>\n</div>\n<ul class=\"entries\" id=\"entries\"></ul>\n<script>\n const ENTRIES = JSON.parse(\\`${escaped}\\`);\n const ul = document.getElementById('entries');\n const lead = document.getElementById('lead');\n const progress = document.getElementById('progress');\n const runBtn = document.getElementById('run');\n const finishBtn = document.getElementById('finish');\n\n lead.textContent = ENTRIES.length + ' article' + (ENTRIES.length === 1 ? '' : 's') + ' to sign. MetaMask will pop up once per article — review the typed data and confirm.';\n progress.textContent = '0 / ' + ENTRIES.length;\n\n const rows = ENTRIES.map((e, i) => {\n const li = document.createElement('li');\n const label = document.createElement('span');\n label.className = 'label';\n label.textContent = (i + 1) + '. ' + e.label;\n const pill = document.createElement('span');\n pill.className = 'pill idle';\n pill.textContent = 'idle';\n li.appendChild(pill);\n li.appendChild(label);\n ul.appendChild(li);\n return { pill, li };\n });\n\n let signed = 0;\n\n async function signOne(i) {\n const entry = ENTRIES[i];\n rows[i].pill.className = 'pill active';\n rows[i].pill.textContent = 'signing';\n\n if (!window.ethereum) throw new Error('No window.ethereum provider');\n\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n let signature;\n try {\n signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(entry.typedData)],\n });\n } catch (err) {\n const msg = err && err.message ? err.message : String(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'rejected';\n const errLi = document.createElement('div');\n errLi.className = 'err-msg';\n errLi.textContent = msg;\n rows[i].li.appendChild(errLi);\n await fetch('/skip', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, reason: msg }),\n });\n return false;\n }\n\n const res = await fetch('/sig', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, signature, signerAddress: signer }),\n });\n if (!res.ok) {\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'server-rejected';\n return false;\n }\n rows[i].pill.className = 'pill signed';\n rows[i].pill.textContent = 'signed';\n signed++;\n progress.textContent = signed + ' / ' + ENTRIES.length;\n return true;\n }\n\n runBtn.addEventListener('click', async () => {\n runBtn.disabled = true;\n for (let i = 0; i < ENTRIES.length; i++) {\n try { await signOne(i); } catch (err) {\n console.error(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'error';\n }\n }\n finishBtn.disabled = false;\n finishBtn.classList.add('primary');\n });\n\n finishBtn.addEventListener('click', async () => {\n finishBtn.disabled = true;\n await fetch('/done', { method: 'POST' });\n document.body.innerHTML = '<h1>Done.</h1><p>You can close this tab.</p>';\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer } from 'node:http';\nimport { type AddressInfo } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildBatchSignPageHtml } from './sign-batch-page-html.js';\n\nexport type BatchSignEntry = {\n /** Unique key — used to correlate signatures back to entries. */\n id: string;\n /** Human-readable label shown in the browser UI. */\n label: string;\n /** EIP-712 message to sign. */\n message: AttestationMessage;\n};\n\nexport type BatchSignFlowOptions = {\n entries: BatchSignEntry[];\n onUrlReady?: (url: string) => Promise<void> | void;\n /** Total session timeout. Default 30 minutes. */\n timeoutMs?: number;\n};\n\nexport type BatchSignature = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nexport type BatchSignFlowResult = {\n signatures: Map<string, BatchSignature>;\n /** Entries the user explicitly skipped or that failed (rejected wallet, etc.). */\n skipped: Map<string, string>;\n};\n\nconst DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;\n\nexport async function signBatchViaBrowser(\n opts: BatchSignFlowOptions,\n): Promise<BatchSignFlowResult> {\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const wireEntries = opts.entries.map((e) => ({\n id: e.id,\n label: e.label,\n typedData: {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(e.message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n },\n }));\n const html = buildBatchSignPageHtml(wireEntries);\n\n const signatures = new Map<string, BatchSignature>();\n const skipped = new Map<string, string>();\n\n return new Promise<BatchSignFlowResult>((resolve, reject) => {\n const server = createServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && (req.url === '/sig' || req.url === '/skip')) {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: {\n id: string;\n signature?: `0x${string}`;\n signerAddress?: `0x${string}`;\n reason?: string;\n };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return;\n }\n if (req.url === '/sig') {\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return;\n }\n signatures.set(parsed.id, {\n signature: parsed.signature,\n signerAddress: parsed.signerAddress,\n });\n } else {\n skipped.set(parsed.id, parsed.reason ?? 'skipped');\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n });\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve({ signatures, skipped });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Batch sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.js","names":[],"sources":["../src/eth/sign-batch-page-html.ts","../src/eth/sign-batch-flow.ts"],"sourcesContent":["/**\n * HTML page served by the local CLI server during batch signing. Loads the\n * full list of EIP-712 typed-data messages, walks them sequentially, and POSTs\n * each signature back as MetaMask returns it. The server closes after the user\n * either signs the last entry or clicks \"Done\".\n */\nexport function buildBatchSignPageHtml(\n entries: Array<{ id: string; label: string; typedData: object }>,\n): string {\n const escaped = JSON.stringify(entries)\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign all attestations</title>\n<style>\n * { box-sizing: border-box; }\n body { font-family: system-ui, sans-serif; max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.5; color: #111; }\n h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }\n .lead { color: #666; margin: 0 0 2rem; }\n .controls { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; align-items: center; }\n button { padding: 0.6rem 1rem; font-size: 0.95rem; cursor: pointer; border: 1px solid #ccc; background: #fff; border-radius: 6px; }\n button.primary { background: #111; color: #fff; border-color: #111; }\n button:disabled { opacity: 0.5; cursor: not-allowed; }\n .progress { font-size: 0.85rem; color: #666; }\n ul.entries { list-style: none; padding: 0; margin: 0; border-top: 1px solid #eee; }\n ul.entries li { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid #eee; font-size: 0.9rem; }\n .pill { font-family: ui-monospace, monospace; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; padding: 2px 8px; border-radius: 999px; border: 1px solid; }\n .pill.idle { color: #999; border-color: #ddd; }\n .pill.active { color: #0070f3; border-color: #c6dfff; background: #f0f7ff; }\n .pill.signed { color: #0a7d22; border-color: #b8e6c1; background: #f0faf3; }\n .pill.skipped { color: #b78403; border-color: #f1d57f; background: #fffaeb; }\n .pill.error { color: #b00020; border-color: #f5b8c1; background: #fef0f2; }\n .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .err-msg { font-size: 0.75rem; color: #b00020; padding: 0 0 0.5rem 0; }\n pre.preview { max-height: 200px; overflow: auto; font-size: 0.8rem; background: #f7f7f7; padding: 0.75rem; border-radius: 6px; margin: 0 0 1.5rem; }\n</style>\n</head>\n<body>\n<h1>Sign attestations</h1>\n<p class=\"lead\" id=\"lead\"></p>\n<div class=\"controls\">\n <button class=\"primary\" id=\"run\">Sign all</button>\n <button id=\"finish\" disabled>Done</button>\n <span class=\"progress\" id=\"progress\"></span>\n</div>\n<ul class=\"entries\" id=\"entries\"></ul>\n<script>\n const ENTRIES = JSON.parse(\\`${escaped}\\`);\n const ul = document.getElementById('entries');\n const lead = document.getElementById('lead');\n const progress = document.getElementById('progress');\n const runBtn = document.getElementById('run');\n const finishBtn = document.getElementById('finish');\n\n lead.textContent = ENTRIES.length + ' article' + (ENTRIES.length === 1 ? '' : 's') + ' to sign. MetaMask will pop up once per article — review the typed data and confirm.';\n progress.textContent = '0 / ' + ENTRIES.length;\n\n const rows = ENTRIES.map((e, i) => {\n const li = document.createElement('li');\n const label = document.createElement('span');\n label.className = 'label';\n label.textContent = (i + 1) + '. ' + e.label;\n const pill = document.createElement('span');\n pill.className = 'pill idle';\n pill.textContent = 'idle';\n li.appendChild(pill);\n li.appendChild(label);\n ul.appendChild(li);\n return { pill, li };\n });\n\n let signed = 0;\n\n async function signOne(i) {\n const entry = ENTRIES[i];\n rows[i].pill.className = 'pill active';\n rows[i].pill.textContent = 'signing';\n\n if (!window.ethereum) throw new Error('No window.ethereum provider');\n\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n let signature;\n try {\n signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(entry.typedData)],\n });\n } catch (err) {\n const msg = err && err.message ? err.message : String(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'rejected';\n const errLi = document.createElement('div');\n errLi.className = 'err-msg';\n errLi.textContent = msg;\n rows[i].li.appendChild(errLi);\n await fetch('/skip', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, reason: msg }),\n });\n return false;\n }\n\n const res = await fetch('/sig', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id, signature, signerAddress: signer }),\n });\n if (!res.ok) {\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'server-rejected';\n return false;\n }\n rows[i].pill.className = 'pill signed';\n rows[i].pill.textContent = 'signed';\n signed++;\n progress.textContent = signed + ' / ' + ENTRIES.length;\n return true;\n }\n\n runBtn.addEventListener('click', async () => {\n runBtn.disabled = true;\n for (let i = 0; i < ENTRIES.length; i++) {\n try { await signOne(i); } catch (err) {\n console.error(err);\n rows[i].pill.className = 'pill error';\n rows[i].pill.textContent = 'error';\n }\n }\n finishBtn.disabled = false;\n finishBtn.classList.add('primary');\n });\n\n finishBtn.addEventListener('click', async () => {\n finishBtn.disabled = true;\n await fetch('/done', { method: 'POST' });\n document.body.innerHTML = '<h1>Done.</h1><p>You can close this tab.</p>';\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer } from 'node:http';\nimport { type AddressInfo } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildBatchSignPageHtml } from './sign-batch-page-html.js';\n\nexport type BatchSignEntry = {\n /** Unique key — used to correlate signatures back to entries. */\n id: string;\n /** Human-readable label shown in the browser UI. */\n label: string;\n /** EIP-712 message to sign. */\n message: AttestationMessage;\n};\n\nexport type BatchSignFlowOptions = {\n entries: BatchSignEntry[];\n onUrlReady?: (url: string) => Promise<void> | void;\n /** Total session timeout. Default 30 minutes. */\n timeoutMs?: number;\n};\n\nexport type BatchSignature = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nexport type BatchSignFlowResult = {\n signatures: Map<string, BatchSignature>;\n /** Entries the user explicitly skipped or that failed (rejected wallet, etc.). */\n skipped: Map<string, string>;\n};\n\nconst DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;\n\nexport async function signBatchViaBrowser(\n opts: BatchSignFlowOptions,\n): Promise<BatchSignFlowResult> {\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const wireEntries = opts.entries.map((e) => ({\n id: e.id,\n label: e.label,\n typedData: {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(e.message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n },\n }));\n const html = buildBatchSignPageHtml(wireEntries);\n\n const signatures = new Map<string, BatchSignature>();\n const skipped = new Map<string, string>();\n\n return new Promise<BatchSignFlowResult>((resolve, reject) => {\n const server = createServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && (req.url === '/sig' || req.url === '/skip')) {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: {\n id: string;\n signature?: `0x${string}`;\n signerAddress?: `0x${string}`;\n reason?: string;\n };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return;\n }\n if (req.url === '/sig') {\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return;\n }\n signatures.set(parsed.id, {\n signature: parsed.signature,\n signerAddress: parsed.signerAddress,\n });\n } else {\n skipped.set(parsed.id, parsed.reason ?? 'skipped');\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n });\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve({ signatures, skipped });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Batch sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n"],"mappings":"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"}
@@ -15,7 +15,7 @@ var __copyProps = (to, from, except, desc) => {
15
15
  }
16
16
  return to;
17
17
  };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
19
  value: mod,
20
20
  enumerable: true
21
21
  }) : target, mod));
@@ -191,7 +191,7 @@ function buildSignPageHtml(typedDataJson) {
191
191
  }
192
192
  //#endregion
193
193
  //#region src/eth/sign-flow.ts
194
- const DEFAULT_TIMEOUT_MS = 300 * 1e3;
194
+ const DEFAULT_TIMEOUT_MS = 3e5;
195
195
  /**
196
196
  * Spin up a one-shot localhost HTTP server, open the browser, wait for the user
197
197
  * to sign with their wallet, return the signature.
@@ -1 +1 @@
1
- {"version":3,"file":"sign-flow.cjs","names":["OpenTimestamps","DetachedTimestampFile","Ops","OpenTimestamps","ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1"],"sources":["../src/ots/stamp.ts","../src/ots/verify.ts","../src/eth/sign-page-html.ts","../src/eth/sign-flow.ts"],"sourcesContent":["import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n stamp: (detached: unknown) => Promise<void>;\n upgrade: (detached: unknown) => Promise<boolean>;\n verify: (\n detached: unknown,\n original: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Submit a SHA-256 digest to OpenTimestamps calendars and return the proof bytes.\n *\n * The returned proof initially contains only calendar attestations. Run upgrade()\n * after ~24h to anchor it in a Bitcoin block.\n */\nexport async function stampDigest(digest: Uint8Array): Promise<Uint8Array> {\n if (digest.length !== 32) {\n throw new Error(`Expected 32-byte SHA-256 digest, got ${digest.length} bytes`);\n }\n\n const detached = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n await (OpenTimestamps as unknown as { stamp: (d: unknown) => Promise<void> }).stamp(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return new Uint8Array(bytes as Buffer);\n}\n\n/**\n * Try to upgrade a calendar-only proof to a Bitcoin-anchored proof.\n *\n * Returns the (possibly upgraded) proof bytes plus a flag indicating whether\n * the upgrade actually attached a Bitcoin attestation this time.\n */\nexport async function upgradeProof(\n otsBytes: Uint8Array,\n): Promise<{ bytes: Uint8Array; upgraded: boolean }> {\n const detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n const upgraded = await (\n OpenTimestamps as unknown as { upgrade: (d: unknown) => Promise<boolean> }\n ).upgrade(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return { bytes: new Uint8Array(bytes as Buffer), upgraded };\n}\n","import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Options for {@link verifyOts}.\n *\n * `explorerUrl` points the Bitcoin block lookup at an alternative Esplora-\n * compatible endpoint (e.g. a self-hosted explorer, or a test stub). When\n * absent the library's default public source is used.\n */\nexport type OtsVerifyOptions = {\n explorerUrl?: string;\n};\n\nexport type OtsVerifyOk = {\n ok: true;\n bitcoinBlockTime: Date;\n};\n\nexport type OtsVerifyFail = {\n ok: false;\n reason: 'digest-mismatch' | 'invalid-proof' | 'pending-bitcoin';\n details?: string;\n};\n\nexport type OtsVerifyResult = OtsVerifyFail | OtsVerifyOk;\n\n/**\n * Verify an OTS proof attests the given digest, and return the Bitcoin block\n * attestation time if present.\n *\n * Network: this calls a Bitcoin block-info source via the OTS library to\n * validate the Merkle path — the library's default public source, or the\n * Esplora-compatible endpoint given in `options.explorerUrl`. In\n * production-paranoid mode you should run your own Bitcoin node and point\n * the explorer option at it.\n */\nexport async function verifyOts(\n digest: Uint8Array,\n otsBytes: Uint8Array,\n options?: OtsVerifyOptions,\n): Promise<OtsVerifyResult> {\n if (digest.length !== 32) {\n return {\n details: `Expected 32-byte digest, got ${digest.length}`,\n ok: false,\n reason: 'digest-mismatch',\n };\n }\n\n let detached: unknown;\n let original: unknown;\n try {\n detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n original = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n // An explicit explorer forces the Esplora path — no local Bitcoin node\n // Probing — so the lookup goes exactly where the caller pointed it.\n // The lib's docs name the flag `ignore_bitcoin_node` but the code checks\n // `ignoreBitcoinNode` — pass both so either side of that drift works.\n // The esplora timeout is documented as seconds but flows straight into\n // Request-promise, which reads milliseconds — so this is 10s, not 10000s.\n const verifyOptions = options?.explorerUrl\n ? {\n esplora: { timeout: 10_000, url: options.explorerUrl },\n ignore_bitcoin_node: true,\n ignoreBitcoinNode: true,\n }\n : undefined;\n\n let attestations: Record<string, { timestamp: number }>;\n try {\n const ots = OpenTimestamps as unknown as {\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n };\n attestations = verifyOptions\n ? await ots.verify(detached, original, verifyOptions)\n : await ots.verify(detached, original);\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n const bitcoin = attestations['bitcoin'];\n if (bitcoin === undefined) {\n return { ok: false, reason: 'pending-bitcoin' };\n }\n\n return {\n bitcoinBlockTime: new Date(bitcoin.timestamp * 1000),\n ok: true,\n };\n}\n","/**\n * Self-contained HTML page served by the local CLI server. Opens MetaMask\n * (or any window.ethereum provider), requests EIP-712 v4 signature, POSTs\n * the result back to the localhost callback.\n *\n * No bundler, no external script — just a string template. Keeping this\n * pure-string makes it trivial to audit what gets shown to the wallet.\n */\nexport function buildSignPageHtml(typedDataJson: string): string {\n // TypedDataJson MUST already be a JSON string of EIP-712 v4 typed data\n // (with `domain`, `types`, `primaryType`, `message`).\n const escaped = typedDataJson\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign attestation</title>\n<style>\n body { font-family: system-ui, sans-serif; max-width: 640px; margin: 4rem auto; padding: 0 1rem; line-height: 1.5; }\n button { padding: 0.75rem 1.5rem; font-size: 1rem; cursor: pointer; }\n pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; font-size: 0.85rem; }\n .ok { color: #0a7d22; }\n .err { color: #b00020; white-space: pre-wrap; }\n</style>\n</head>\n<body>\n<h1>Sign attestation</h1>\n<p>Review the structured data below, then click <strong>Sign</strong>. Your wallet will show the same fields.</p>\n<pre id=\"preview\"></pre>\n<p><button id=\"sign\">Sign with wallet</button></p>\n<p id=\"status\"></p>\n<script>\n const TYPED_DATA = JSON.parse(\\`${escaped}\\`);\n document.getElementById('preview').textContent = JSON.stringify(TYPED_DATA.message, null, 2);\n\n document.getElementById('sign').addEventListener('click', async () => {\n const status = document.getElementById('status');\n status.className = '';\n status.textContent = 'Requesting account…';\n\n if (!window.ethereum) {\n status.className = 'err';\n status.textContent = 'No window.ethereum provider found. Install MetaMask (or another wallet) and reload.';\n return;\n }\n\n try {\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n status.textContent = 'Awaiting wallet confirmation…';\n const signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(TYPED_DATA)],\n });\n\n status.textContent = 'Submitting signature…';\n const res = await fetch('/done', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ signature, signerAddress: signer }),\n });\n if (!res.ok) throw new Error('Server rejected the signature: HTTP ' + res.status);\n\n status.className = 'ok';\n status.textContent = 'Signed. You can close this tab.';\n document.getElementById('sign').disabled = true;\n } catch (err) {\n status.className = 'err';\n status.textContent = 'Failed: ' + (err && err.message ? err.message : String(err));\n }\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer as createHttpServer } from 'node:http';\nimport { type AddressInfo, createServer } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildSignPageHtml } from './sign-page-html.js';\n\nexport type SignFlowOptions = {\n message: AttestationMessage;\n onUrlReady?: (url: string) => Promise<void> | void;\n timeoutMs?: number;\n};\n\nexport type SignFlowResult = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Spin up a one-shot localhost HTTP server, open the browser, wait for the user\n * to sign with their wallet, return the signature.\n *\n * The local server only accepts loopback requests (127.0.0.1).\n */\nexport async function signViaBrowser(opts: SignFlowOptions): Promise<SignFlowResult> {\n const message = opts.message;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const typedData = {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n };\n const html = buildSignPageHtml(JSON.stringify(typedData));\n\n return new Promise<SignFlowResult>((resolve, reject) => {\n const server = createHttpServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: { signature: `0x${string}`; signerAddress: `0x${string}` };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return; // Keep server running so user can retry\n }\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return; // Keep server running so user can retry\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve(parsed);\n });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\n/**\n * Convert AttestationMessage (with bigint publishedAt) to a JSON-serializable\n * form for the browser. EIP-712 v4 expects numeric strings for uint64.\n */\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n\n// Suppress unused-import lint: createServer kept for future net-level helpers.\nvoid createServer;\n"],"mappings":"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"}
1
+ {"version":3,"file":"sign-flow.cjs","names":["OpenTimestamps","DetachedTimestampFile","Ops","OpenTimestamps","ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1","createHttpServer"],"sources":["../src/ots/stamp.ts","../src/ots/verify.ts","../src/eth/sign-page-html.ts","../src/eth/sign-flow.ts"],"sourcesContent":["import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n stamp: (detached: unknown) => Promise<void>;\n upgrade: (detached: unknown) => Promise<boolean>;\n verify: (\n detached: unknown,\n original: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Submit a SHA-256 digest to OpenTimestamps calendars and return the proof bytes.\n *\n * The returned proof initially contains only calendar attestations. Run upgrade()\n * after ~24h to anchor it in a Bitcoin block.\n */\nexport async function stampDigest(digest: Uint8Array): Promise<Uint8Array> {\n if (digest.length !== 32) {\n throw new Error(`Expected 32-byte SHA-256 digest, got ${digest.length} bytes`);\n }\n\n const detached = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n await (OpenTimestamps as unknown as { stamp: (d: unknown) => Promise<void> }).stamp(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return new Uint8Array(bytes as Buffer);\n}\n\n/**\n * Try to upgrade a calendar-only proof to a Bitcoin-anchored proof.\n *\n * Returns the (possibly upgraded) proof bytes plus a flag indicating whether\n * the upgrade actually attached a Bitcoin attestation this time.\n */\nexport async function upgradeProof(\n otsBytes: Uint8Array,\n): Promise<{ bytes: Uint8Array; upgraded: boolean }> {\n const detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n const upgraded = await (\n OpenTimestamps as unknown as { upgrade: (d: unknown) => Promise<boolean> }\n ).upgrade(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return { bytes: new Uint8Array(bytes as Buffer), upgraded };\n}\n","import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Options for {@link verifyOts}.\n *\n * `explorerUrl` points the Bitcoin block lookup at an alternative Esplora-\n * compatible endpoint (e.g. a self-hosted explorer, or a test stub). When\n * absent the library's default public source is used.\n */\nexport type OtsVerifyOptions = {\n explorerUrl?: string;\n};\n\nexport type OtsVerifyOk = {\n ok: true;\n bitcoinBlockTime: Date;\n};\n\nexport type OtsVerifyFail = {\n ok: false;\n reason: 'digest-mismatch' | 'invalid-proof' | 'pending-bitcoin';\n details?: string;\n};\n\nexport type OtsVerifyResult = OtsVerifyFail | OtsVerifyOk;\n\n/**\n * Verify an OTS proof attests the given digest, and return the Bitcoin block\n * attestation time if present.\n *\n * Network: this calls a Bitcoin block-info source via the OTS library to\n * validate the Merkle path — the library's default public source, or the\n * Esplora-compatible endpoint given in `options.explorerUrl`. In\n * production-paranoid mode you should run your own Bitcoin node and point\n * the explorer option at it.\n */\nexport async function verifyOts(\n digest: Uint8Array,\n otsBytes: Uint8Array,\n options?: OtsVerifyOptions,\n): Promise<OtsVerifyResult> {\n if (digest.length !== 32) {\n return {\n details: `Expected 32-byte digest, got ${digest.length}`,\n ok: false,\n reason: 'digest-mismatch',\n };\n }\n\n let detached: unknown;\n let original: unknown;\n try {\n detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n original = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n // An explicit explorer forces the Esplora path — no local Bitcoin node\n // Probing — so the lookup goes exactly where the caller pointed it.\n // The lib's docs name the flag `ignore_bitcoin_node` but the code checks\n // `ignoreBitcoinNode` — pass both so either side of that drift works.\n // The esplora timeout is documented as seconds but flows straight into\n // Request-promise, which reads milliseconds — so this is 10s, not 10000s.\n const verifyOptions = options?.explorerUrl\n ? {\n esplora: { timeout: 10_000, url: options.explorerUrl },\n ignore_bitcoin_node: true,\n ignoreBitcoinNode: true,\n }\n : undefined;\n\n let attestations: Record<string, { timestamp: number }>;\n try {\n const ots = OpenTimestamps as unknown as {\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n };\n attestations = verifyOptions\n ? await ots.verify(detached, original, verifyOptions)\n : await ots.verify(detached, original);\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n const bitcoin = attestations['bitcoin'];\n if (bitcoin === undefined) {\n return { ok: false, reason: 'pending-bitcoin' };\n }\n\n return {\n bitcoinBlockTime: new Date(bitcoin.timestamp * 1000),\n ok: true,\n };\n}\n","/**\n * Self-contained HTML page served by the local CLI server. Opens MetaMask\n * (or any window.ethereum provider), requests EIP-712 v4 signature, POSTs\n * the result back to the localhost callback.\n *\n * No bundler, no external script — just a string template. Keeping this\n * pure-string makes it trivial to audit what gets shown to the wallet.\n */\nexport function buildSignPageHtml(typedDataJson: string): string {\n // TypedDataJson MUST already be a JSON string of EIP-712 v4 typed data\n // (with `domain`, `types`, `primaryType`, `message`).\n const escaped = typedDataJson\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign attestation</title>\n<style>\n body { font-family: system-ui, sans-serif; max-width: 640px; margin: 4rem auto; padding: 0 1rem; line-height: 1.5; }\n button { padding: 0.75rem 1.5rem; font-size: 1rem; cursor: pointer; }\n pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; font-size: 0.85rem; }\n .ok { color: #0a7d22; }\n .err { color: #b00020; white-space: pre-wrap; }\n</style>\n</head>\n<body>\n<h1>Sign attestation</h1>\n<p>Review the structured data below, then click <strong>Sign</strong>. Your wallet will show the same fields.</p>\n<pre id=\"preview\"></pre>\n<p><button id=\"sign\">Sign with wallet</button></p>\n<p id=\"status\"></p>\n<script>\n const TYPED_DATA = JSON.parse(\\`${escaped}\\`);\n document.getElementById('preview').textContent = JSON.stringify(TYPED_DATA.message, null, 2);\n\n document.getElementById('sign').addEventListener('click', async () => {\n const status = document.getElementById('status');\n status.className = '';\n status.textContent = 'Requesting account…';\n\n if (!window.ethereum) {\n status.className = 'err';\n status.textContent = 'No window.ethereum provider found. Install MetaMask (or another wallet) and reload.';\n return;\n }\n\n try {\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n status.textContent = 'Awaiting wallet confirmation…';\n const signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(TYPED_DATA)],\n });\n\n status.textContent = 'Submitting signature…';\n const res = await fetch('/done', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ signature, signerAddress: signer }),\n });\n if (!res.ok) throw new Error('Server rejected the signature: HTTP ' + res.status);\n\n status.className = 'ok';\n status.textContent = 'Signed. You can close this tab.';\n document.getElementById('sign').disabled = true;\n } catch (err) {\n status.className = 'err';\n status.textContent = 'Failed: ' + (err && err.message ? err.message : String(err));\n }\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer as createHttpServer } from 'node:http';\nimport { type AddressInfo, createServer } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildSignPageHtml } from './sign-page-html.js';\n\nexport type SignFlowOptions = {\n message: AttestationMessage;\n onUrlReady?: (url: string) => Promise<void> | void;\n timeoutMs?: number;\n};\n\nexport type SignFlowResult = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Spin up a one-shot localhost HTTP server, open the browser, wait for the user\n * to sign with their wallet, return the signature.\n *\n * The local server only accepts loopback requests (127.0.0.1).\n */\nexport async function signViaBrowser(opts: SignFlowOptions): Promise<SignFlowResult> {\n const message = opts.message;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const typedData = {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n };\n const html = buildSignPageHtml(JSON.stringify(typedData));\n\n return new Promise<SignFlowResult>((resolve, reject) => {\n const server = createHttpServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: { signature: `0x${string}`; signerAddress: `0x${string}` };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return; // Keep server running so user can retry\n }\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return; // Keep server running so user can retry\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve(parsed);\n });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\n/**\n * Convert AttestationMessage (with bigint publishedAt) to a JSON-serializable\n * form for the browser. EIP-712 v4 expects numeric strings for uint64.\n */\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n\n// Suppress unused-import lint: createServer kept for future net-level helpers.\nvoid createServer;\n"],"mappings":"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"}
package/dist/sign-flow.js CHANGED
@@ -168,7 +168,7 @@ function buildSignPageHtml(typedDataJson) {
168
168
  }
169
169
  //#endregion
170
170
  //#region src/eth/sign-flow.ts
171
- const DEFAULT_TIMEOUT_MS = 300 * 1e3;
171
+ const DEFAULT_TIMEOUT_MS = 3e5;
172
172
  /**
173
173
  * Spin up a one-shot localhost HTTP server, open the browser, wait for the user
174
174
  * to sign with their wallet, return the signature.
@@ -1 +1 @@
1
- {"version":3,"file":"sign-flow.js","names":["DetachedTimestampFile","Ops","createHttpServer"],"sources":["../src/ots/stamp.ts","../src/ots/verify.ts","../src/eth/sign-page-html.ts","../src/eth/sign-flow.ts"],"sourcesContent":["import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n stamp: (detached: unknown) => Promise<void>;\n upgrade: (detached: unknown) => Promise<boolean>;\n verify: (\n detached: unknown,\n original: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Submit a SHA-256 digest to OpenTimestamps calendars and return the proof bytes.\n *\n * The returned proof initially contains only calendar attestations. Run upgrade()\n * after ~24h to anchor it in a Bitcoin block.\n */\nexport async function stampDigest(digest: Uint8Array): Promise<Uint8Array> {\n if (digest.length !== 32) {\n throw new Error(`Expected 32-byte SHA-256 digest, got ${digest.length} bytes`);\n }\n\n const detached = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n await (OpenTimestamps as unknown as { stamp: (d: unknown) => Promise<void> }).stamp(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return new Uint8Array(bytes as Buffer);\n}\n\n/**\n * Try to upgrade a calendar-only proof to a Bitcoin-anchored proof.\n *\n * Returns the (possibly upgraded) proof bytes plus a flag indicating whether\n * the upgrade actually attached a Bitcoin attestation this time.\n */\nexport async function upgradeProof(\n otsBytes: Uint8Array,\n): Promise<{ bytes: Uint8Array; upgraded: boolean }> {\n const detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n const upgraded = await (\n OpenTimestamps as unknown as { upgrade: (d: unknown) => Promise<boolean> }\n ).upgrade(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return { bytes: new Uint8Array(bytes as Buffer), upgraded };\n}\n","import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Options for {@link verifyOts}.\n *\n * `explorerUrl` points the Bitcoin block lookup at an alternative Esplora-\n * compatible endpoint (e.g. a self-hosted explorer, or a test stub). When\n * absent the library's default public source is used.\n */\nexport type OtsVerifyOptions = {\n explorerUrl?: string;\n};\n\nexport type OtsVerifyOk = {\n ok: true;\n bitcoinBlockTime: Date;\n};\n\nexport type OtsVerifyFail = {\n ok: false;\n reason: 'digest-mismatch' | 'invalid-proof' | 'pending-bitcoin';\n details?: string;\n};\n\nexport type OtsVerifyResult = OtsVerifyFail | OtsVerifyOk;\n\n/**\n * Verify an OTS proof attests the given digest, and return the Bitcoin block\n * attestation time if present.\n *\n * Network: this calls a Bitcoin block-info source via the OTS library to\n * validate the Merkle path — the library's default public source, or the\n * Esplora-compatible endpoint given in `options.explorerUrl`. In\n * production-paranoid mode you should run your own Bitcoin node and point\n * the explorer option at it.\n */\nexport async function verifyOts(\n digest: Uint8Array,\n otsBytes: Uint8Array,\n options?: OtsVerifyOptions,\n): Promise<OtsVerifyResult> {\n if (digest.length !== 32) {\n return {\n details: `Expected 32-byte digest, got ${digest.length}`,\n ok: false,\n reason: 'digest-mismatch',\n };\n }\n\n let detached: unknown;\n let original: unknown;\n try {\n detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n original = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n // An explicit explorer forces the Esplora path — no local Bitcoin node\n // Probing — so the lookup goes exactly where the caller pointed it.\n // The lib's docs name the flag `ignore_bitcoin_node` but the code checks\n // `ignoreBitcoinNode` — pass both so either side of that drift works.\n // The esplora timeout is documented as seconds but flows straight into\n // Request-promise, which reads milliseconds — so this is 10s, not 10000s.\n const verifyOptions = options?.explorerUrl\n ? {\n esplora: { timeout: 10_000, url: options.explorerUrl },\n ignore_bitcoin_node: true,\n ignoreBitcoinNode: true,\n }\n : undefined;\n\n let attestations: Record<string, { timestamp: number }>;\n try {\n const ots = OpenTimestamps as unknown as {\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n };\n attestations = verifyOptions\n ? await ots.verify(detached, original, verifyOptions)\n : await ots.verify(detached, original);\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n const bitcoin = attestations['bitcoin'];\n if (bitcoin === undefined) {\n return { ok: false, reason: 'pending-bitcoin' };\n }\n\n return {\n bitcoinBlockTime: new Date(bitcoin.timestamp * 1000),\n ok: true,\n };\n}\n","/**\n * Self-contained HTML page served by the local CLI server. Opens MetaMask\n * (or any window.ethereum provider), requests EIP-712 v4 signature, POSTs\n * the result back to the localhost callback.\n *\n * No bundler, no external script — just a string template. Keeping this\n * pure-string makes it trivial to audit what gets shown to the wallet.\n */\nexport function buildSignPageHtml(typedDataJson: string): string {\n // TypedDataJson MUST already be a JSON string of EIP-712 v4 typed data\n // (with `domain`, `types`, `primaryType`, `message`).\n const escaped = typedDataJson\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign attestation</title>\n<style>\n body { font-family: system-ui, sans-serif; max-width: 640px; margin: 4rem auto; padding: 0 1rem; line-height: 1.5; }\n button { padding: 0.75rem 1.5rem; font-size: 1rem; cursor: pointer; }\n pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; font-size: 0.85rem; }\n .ok { color: #0a7d22; }\n .err { color: #b00020; white-space: pre-wrap; }\n</style>\n</head>\n<body>\n<h1>Sign attestation</h1>\n<p>Review the structured data below, then click <strong>Sign</strong>. Your wallet will show the same fields.</p>\n<pre id=\"preview\"></pre>\n<p><button id=\"sign\">Sign with wallet</button></p>\n<p id=\"status\"></p>\n<script>\n const TYPED_DATA = JSON.parse(\\`${escaped}\\`);\n document.getElementById('preview').textContent = JSON.stringify(TYPED_DATA.message, null, 2);\n\n document.getElementById('sign').addEventListener('click', async () => {\n const status = document.getElementById('status');\n status.className = '';\n status.textContent = 'Requesting account…';\n\n if (!window.ethereum) {\n status.className = 'err';\n status.textContent = 'No window.ethereum provider found. Install MetaMask (or another wallet) and reload.';\n return;\n }\n\n try {\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n status.textContent = 'Awaiting wallet confirmation…';\n const signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(TYPED_DATA)],\n });\n\n status.textContent = 'Submitting signature…';\n const res = await fetch('/done', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ signature, signerAddress: signer }),\n });\n if (!res.ok) throw new Error('Server rejected the signature: HTTP ' + res.status);\n\n status.className = 'ok';\n status.textContent = 'Signed. You can close this tab.';\n document.getElementById('sign').disabled = true;\n } catch (err) {\n status.className = 'err';\n status.textContent = 'Failed: ' + (err && err.message ? err.message : String(err));\n }\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer as createHttpServer } from 'node:http';\nimport { type AddressInfo, createServer } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildSignPageHtml } from './sign-page-html.js';\n\nexport type SignFlowOptions = {\n message: AttestationMessage;\n onUrlReady?: (url: string) => Promise<void> | void;\n timeoutMs?: number;\n};\n\nexport type SignFlowResult = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Spin up a one-shot localhost HTTP server, open the browser, wait for the user\n * to sign with their wallet, return the signature.\n *\n * The local server only accepts loopback requests (127.0.0.1).\n */\nexport async function signViaBrowser(opts: SignFlowOptions): Promise<SignFlowResult> {\n const message = opts.message;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const typedData = {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n };\n const html = buildSignPageHtml(JSON.stringify(typedData));\n\n return new Promise<SignFlowResult>((resolve, reject) => {\n const server = createHttpServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: { signature: `0x${string}`; signerAddress: `0x${string}` };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return; // Keep server running so user can retry\n }\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return; // Keep server running so user can retry\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve(parsed);\n });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\n/**\n * Convert AttestationMessage (with bigint publishedAt) to a JSON-serializable\n * form for the browser. EIP-712 v4 expects numeric strings for uint64.\n */\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n\n// Suppress unused-import lint: createServer kept for future net-level helpers.\nvoid createServer;\n"],"mappings":"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"}
1
+ {"version":3,"file":"sign-flow.js","names":["DetachedTimestampFile","Ops","createHttpServer"],"sources":["../src/ots/stamp.ts","../src/ots/verify.ts","../src/eth/sign-page-html.ts","../src/eth/sign-flow.ts"],"sourcesContent":["import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n stamp: (detached: unknown) => Promise<void>;\n upgrade: (detached: unknown) => Promise<boolean>;\n verify: (\n detached: unknown,\n original: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Submit a SHA-256 digest to OpenTimestamps calendars and return the proof bytes.\n *\n * The returned proof initially contains only calendar attestations. Run upgrade()\n * after ~24h to anchor it in a Bitcoin block.\n */\nexport async function stampDigest(digest: Uint8Array): Promise<Uint8Array> {\n if (digest.length !== 32) {\n throw new Error(`Expected 32-byte SHA-256 digest, got ${digest.length} bytes`);\n }\n\n const detached = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n await (OpenTimestamps as unknown as { stamp: (d: unknown) => Promise<void> }).stamp(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return new Uint8Array(bytes as Buffer);\n}\n\n/**\n * Try to upgrade a calendar-only proof to a Bitcoin-anchored proof.\n *\n * Returns the (possibly upgraded) proof bytes plus a flag indicating whether\n * the upgrade actually attached a Bitcoin attestation this time.\n */\nexport async function upgradeProof(\n otsBytes: Uint8Array,\n): Promise<{ bytes: Uint8Array; upgraded: boolean }> {\n const detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n const upgraded = await (\n OpenTimestamps as unknown as { upgrade: (d: unknown) => Promise<boolean> }\n ).upgrade(detached);\n\n const serialize = (detached as { serializeToBytes: () => Buffer | Uint8Array })\n .serializeToBytes;\n const bytes = serialize.call(detached);\n return { bytes: new Uint8Array(bytes as Buffer), upgraded };\n}\n","import OpenTimestamps from 'javascript-opentimestamps';\n\nconst { DetachedTimestampFile, Ops } = OpenTimestamps as {\n DetachedTimestampFile: {\n deserialize: (bytes: Buffer | Uint8Array) => unknown;\n fromHash: (op: unknown, hash: Buffer) => unknown;\n };\n Ops: { OpSHA256: unknown };\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n};\n\n/**\n * Options for {@link verifyOts}.\n *\n * `explorerUrl` points the Bitcoin block lookup at an alternative Esplora-\n * compatible endpoint (e.g. a self-hosted explorer, or a test stub). When\n * absent the library's default public source is used.\n */\nexport type OtsVerifyOptions = {\n explorerUrl?: string;\n};\n\nexport type OtsVerifyOk = {\n ok: true;\n bitcoinBlockTime: Date;\n};\n\nexport type OtsVerifyFail = {\n ok: false;\n reason: 'digest-mismatch' | 'invalid-proof' | 'pending-bitcoin';\n details?: string;\n};\n\nexport type OtsVerifyResult = OtsVerifyFail | OtsVerifyOk;\n\n/**\n * Verify an OTS proof attests the given digest, and return the Bitcoin block\n * attestation time if present.\n *\n * Network: this calls a Bitcoin block-info source via the OTS library to\n * validate the Merkle path — the library's default public source, or the\n * Esplora-compatible endpoint given in `options.explorerUrl`. In\n * production-paranoid mode you should run your own Bitcoin node and point\n * the explorer option at it.\n */\nexport async function verifyOts(\n digest: Uint8Array,\n otsBytes: Uint8Array,\n options?: OtsVerifyOptions,\n): Promise<OtsVerifyResult> {\n if (digest.length !== 32) {\n return {\n details: `Expected 32-byte digest, got ${digest.length}`,\n ok: false,\n reason: 'digest-mismatch',\n };\n }\n\n let detached: unknown;\n let original: unknown;\n try {\n detached = DetachedTimestampFile.deserialize(Buffer.from(otsBytes));\n original = DetachedTimestampFile.fromHash(\n new (Ops.OpSHA256 as new () => unknown)(),\n Buffer.from(digest),\n );\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n // An explicit explorer forces the Esplora path — no local Bitcoin node\n // Probing — so the lookup goes exactly where the caller pointed it.\n // The lib's docs name the flag `ignore_bitcoin_node` but the code checks\n // `ignoreBitcoinNode` — pass both so either side of that drift works.\n // The esplora timeout is documented as seconds but flows straight into\n // Request-promise, which reads milliseconds — so this is 10s, not 10000s.\n const verifyOptions = options?.explorerUrl\n ? {\n esplora: { timeout: 10_000, url: options.explorerUrl },\n ignore_bitcoin_node: true,\n ignoreBitcoinNode: true,\n }\n : undefined;\n\n let attestations: Record<string, { timestamp: number }>;\n try {\n const ots = OpenTimestamps as unknown as {\n verify: (\n detached: unknown,\n original: unknown,\n options?: unknown,\n ) => Promise<Record<string, { timestamp: number }>>;\n };\n attestations = verifyOptions\n ? await ots.verify(detached, original, verifyOptions)\n : await ots.verify(detached, original);\n } catch (error) {\n return { details: (error as Error).message, ok: false, reason: 'invalid-proof' };\n }\n\n const bitcoin = attestations['bitcoin'];\n if (bitcoin === undefined) {\n return { ok: false, reason: 'pending-bitcoin' };\n }\n\n return {\n bitcoinBlockTime: new Date(bitcoin.timestamp * 1000),\n ok: true,\n };\n}\n","/**\n * Self-contained HTML page served by the local CLI server. Opens MetaMask\n * (or any window.ethereum provider), requests EIP-712 v4 signature, POSTs\n * the result back to the localhost callback.\n *\n * No bundler, no external script — just a string template. Keeping this\n * pure-string makes it trivial to audit what gets shown to the wallet.\n */\nexport function buildSignPageHtml(typedDataJson: string): string {\n // TypedDataJson MUST already be a JSON string of EIP-712 v4 typed data\n // (with `domain`, `types`, `primaryType`, `message`).\n const escaped = typedDataJson\n .replace(/\\\\/g, String.raw`\\\\`)\n .replace(/`/g, '\\\\`')\n .replace(/<\\/script>/gi, String.raw`<\\/script>`);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Sign attestation</title>\n<style>\n body { font-family: system-ui, sans-serif; max-width: 640px; margin: 4rem auto; padding: 0 1rem; line-height: 1.5; }\n button { padding: 0.75rem 1.5rem; font-size: 1rem; cursor: pointer; }\n pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; font-size: 0.85rem; }\n .ok { color: #0a7d22; }\n .err { color: #b00020; white-space: pre-wrap; }\n</style>\n</head>\n<body>\n<h1>Sign attestation</h1>\n<p>Review the structured data below, then click <strong>Sign</strong>. Your wallet will show the same fields.</p>\n<pre id=\"preview\"></pre>\n<p><button id=\"sign\">Sign with wallet</button></p>\n<p id=\"status\"></p>\n<script>\n const TYPED_DATA = JSON.parse(\\`${escaped}\\`);\n document.getElementById('preview').textContent = JSON.stringify(TYPED_DATA.message, null, 2);\n\n document.getElementById('sign').addEventListener('click', async () => {\n const status = document.getElementById('status');\n status.className = '';\n status.textContent = 'Requesting account…';\n\n if (!window.ethereum) {\n status.className = 'err';\n status.textContent = 'No window.ethereum provider found. Install MetaMask (or another wallet) and reload.';\n return;\n }\n\n try {\n const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });\n const signer = accounts[0];\n\n status.textContent = 'Awaiting wallet confirmation…';\n const signature = await window.ethereum.request({\n method: 'eth_signTypedData_v4',\n params: [signer, JSON.stringify(TYPED_DATA)],\n });\n\n status.textContent = 'Submitting signature…';\n const res = await fetch('/done', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ signature, signerAddress: signer }),\n });\n if (!res.ok) throw new Error('Server rejected the signature: HTTP ' + res.status);\n\n status.className = 'ok';\n status.textContent = 'Signed. You can close this tab.';\n document.getElementById('sign').disabled = true;\n } catch (err) {\n status.className = 'err';\n status.textContent = 'Failed: ' + (err && err.message ? err.message : String(err));\n }\n });\n</script>\n</body>\n</html>`;\n}\n","import { createServer as createHttpServer } from 'node:http';\nimport { type AddressInfo, createServer } from 'node:net';\n\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n} from '../core/eip712-schema.js';\nimport { buildSignPageHtml } from './sign-page-html.js';\n\nexport type SignFlowOptions = {\n message: AttestationMessage;\n onUrlReady?: (url: string) => Promise<void> | void;\n timeoutMs?: number;\n};\n\nexport type SignFlowResult = {\n signature: `0x${string}`;\n signerAddress: `0x${string}`;\n};\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Spin up a one-shot localhost HTTP server, open the browser, wait for the user\n * to sign with their wallet, return the signature.\n *\n * The local server only accepts loopback requests (127.0.0.1).\n */\nexport async function signViaBrowser(opts: SignFlowOptions): Promise<SignFlowResult> {\n const message = opts.message;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const typedData = {\n domain: ATTESTATION_DOMAIN_V1,\n message: serializeMessageForBrowser(message),\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: { EIP712Domain: domainTypes(), ...ATTESTATION_TYPES_V1 },\n };\n const html = buildSignPageHtml(JSON.stringify(typedData));\n\n return new Promise<SignFlowResult>((resolve, reject) => {\n const server = createHttpServer((req, res) => {\n if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n\n if (req.method === 'POST' && req.url === '/done') {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n });\n req.on('end', () => {\n let parsed: { signature: `0x${string}`; signerAddress: `0x${string}` };\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n res.writeHead(400);\n res.end('Parse error');\n return; // Keep server running so user can retry\n }\n if (\n typeof parsed.signature !== 'string' ||\n !/^0x[0-9a-fA-F]{130}$/.test(parsed.signature) ||\n typeof parsed.signerAddress !== 'string' ||\n !/^0x[0-9a-fA-F]{40}$/.test(parsed.signerAddress)\n ) {\n res.writeHead(400);\n res.end('Invalid payload');\n return; // Keep server running so user can retry\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('OK');\n res.on('finish', () => {\n clearTimeout(timer);\n server.close();\n resolve(parsed);\n });\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n const timer = setTimeout(() => {\n server.close();\n reject(new Error(`Sign flow timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n server.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/`;\n const ready = opts.onUrlReady ?? defaultOpenInBrowser;\n void Promise.resolve(ready(url)).catch((error: unknown) => {\n clearTimeout(timer);\n server.close();\n reject(error);\n });\n });\n });\n}\n\nasync function defaultOpenInBrowser(url: string): Promise<void> {\n const { default: open } = await import('open');\n await open(url);\n}\n\n/**\n * Convert AttestationMessage (with bigint publishedAt) to a JSON-serializable\n * form for the browser. EIP-712 v4 expects numeric strings for uint64.\n */\nfunction serializeMessageForBrowser(message: AttestationMessage): Record<string, unknown> {\n return {\n claims: {\n priorAttestation: message.claims.priorAttestation,\n publishedAt: message.claims.publishedAt.toString(),\n revision: message.claims.revision,\n slug: message.claims.slug,\n },\n schemaVersion: message.schemaVersion,\n subject: message.subject,\n };\n}\n\nfunction domainTypes(): Array<{ name: string; type: string }> {\n return [\n { name: 'name', type: 'string' },\n { name: 'version', type: 'string' },\n { name: 'chainId', type: 'uint256' },\n ];\n}\n\n// Suppress unused-import lint: createServer kept for future net-level helpers.\nvoid createServer;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify.cjs","names":["ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1","ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1"],"sources":["../src/version.ts","../src/core/canonicalize.ts","../src/core/sha256.ts","../src/attestation/create.ts","../src/attestation/serialize.ts","../src/attestation/verify.ts"],"sourcesContent":["// Frozen on first published attestation. Bump only by adding v2 alongside v1.\nexport const SCHEMA_VERSION = 1 as const;\nexport const CANONICAL_VERSION = 1 as const;\n","/**\n * Canonicalize an article body for attestation v1.\n *\n * FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires\n * publishing a v2 alongside (and v1 verifiers stay alive forever).\n *\n * Rules applied in order:\n * 1. Reject unpaired UTF-16 surrogates (corrupt input).\n * 2. Strip a leading BOM (U+FEFF) if present.\n * 3. Apply Unicode NFC normalization.\n * 4. Convert CRLF and stray CR to LF.\n * 5. Trim trailing whitespace and append exactly one LF.\n * 6. Encode as UTF-8 bytes.\n *\n * No markdown parsing. No per-line whitespace trim (would break \" \\n\" soft breaks).\n * No tab → space substitution (would break code blocks).\n * No interior whitespace collapsing.\n */\nexport function canonicalize(input: string): Uint8Array {\n assertValidUtf16(input);\n\n let s = input;\n if (s.charCodeAt(0) === 0xfeff) {\n s = s.slice(1);\n }\n s = s.normalize('NFC');\n s = s.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n // \\s with /u flag covers all ECMAScript whitespace, including U+00A0 and U+FEFF.\n s = `${s.replace(/\\s+$/u, '')}\\n`;\n\n return new TextEncoder().encode(s);\n}\n\n/**\n * Version of the canonicalization algorithm that produced bytes.\n * Embedded into every attestation so future verifiers can dispatch correctly.\n */\nexport { CANONICAL_VERSION as canonicalVersion } from '../version.js';\n\nexport class InvalidContentError extends Error {\n override name = 'InvalidContentError';\n}\n\nfunction assertValidUtf16(s: string): void {\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = s.charCodeAt(i + 1);\n if (!(next >= 0xdc00 && next <= 0xdfff)) {\n throw new InvalidContentError(`Unpaired high surrogate at index ${i}`);\n }\n i++;\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n throw new InvalidContentError(`Unpaired low surrogate at index ${i}`);\n }\n }\n}\n","import { sha256 } from '@noble/hashes/sha2.js';\n\n/**\n * SHA-256 of bytes, returned as bare hex (no 0x prefix).\n *\n * Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,\n * Deno, and every modern browser. This is the only hash primitive used by the\n * package, so swapping the implementation here is the single point of change.\n */\nexport function sha256Hex(bytes: Uint8Array): string {\n const out = sha256(bytes);\n let hex = '';\n for (const byte of out) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return hex;\n}\n","import { type LocalAccount } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n NO_PRIOR_ATTESTATION,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation } from './types.js';\n\nexport type CreateAttestationInput = {\n content: string;\n title: string;\n slug: string;\n locale: string;\n publishedAt: bigint | Date | number;\n revision?: number;\n priorAttestation?: `0x${string}`;\n};\n\nexport function buildAttestationMessage(input: CreateAttestationInput): AttestationMessage {\n const bytes = canonicalize(input.content);\n const digest = sha256Hex(bytes);\n\n return {\n claims: {\n priorAttestation: input.priorAttestation ?? NO_PRIOR_ATTESTATION,\n publishedAt: toUnixSeconds(input.publishedAt),\n revision: input.revision ?? 1,\n slug: input.slug,\n },\n schemaVersion: SCHEMA_VERSION,\n subject: {\n contentDigest: `0x${digest}` as const,\n locale: input.locale,\n title: input.title,\n },\n };\n}\n\nexport async function signAttestation(\n message: AttestationMessage,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n const signature = await account.signTypedData({\n domain: ATTESTATION_DOMAIN_V1,\n message,\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: ATTESTATION_TYPES_V1,\n });\n\n return {\n ...message,\n signature,\n signerAddress: account.address,\n };\n}\n\nexport async function createAttestation(\n input: CreateAttestationInput,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n return signAttestation(buildAttestationMessage(input), account);\n}\n\nfunction toUnixSeconds(value: bigint | Date | number): bigint {\n if (typeof value === 'bigint') {\n return value;\n }\n if (typeof value === 'number') {\n return BigInt(Math.floor(value));\n }\n return BigInt(Math.floor(value.getTime() / 1000));\n}\n","import { type SignedAttestation, type StoredAttestation } from './types.js';\n\nexport function toStored(att: SignedAttestation): StoredAttestation {\n return {\n claims: {\n priorAttestation: att.claims.priorAttestation,\n publishedAt: att.claims.publishedAt.toString(),\n revision: att.claims.revision,\n slug: att.claims.slug,\n },\n schemaVersion: att.schemaVersion,\n signature: att.signature,\n signerAddress: att.signerAddress,\n subject: att.subject,\n };\n}\n\nexport function fromStored(stored: StoredAttestation): SignedAttestation {\n return {\n claims: {\n priorAttestation: stored.claims.priorAttestation,\n publishedAt: BigInt(stored.claims.publishedAt),\n revision: stored.claims.revision,\n slug: stored.claims.slug,\n },\n schemaVersion: stored.schemaVersion,\n signature: stored.signature,\n signerAddress: stored.signerAddress,\n subject: stored.subject,\n };\n}\n\nexport function stringify(att: SignedAttestation): string {\n return `${JSON.stringify(toStored(att), null, 2)}\\n`;\n}\n\nexport function parse(json: string): SignedAttestation {\n const parsed = JSON.parse(json) as StoredAttestation;\n return fromStored(parsed);\n}\n","import { recoverTypedDataAddress } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation, type VerifyResult } from './types.js';\n\nexport type VerifyInput = {\n content: string;\n attestation: SignedAttestation;\n};\n\nexport async function verifyAttestation(input: VerifyInput): Promise<VerifyResult> {\n const att = input.attestation;\n\n if (att.schemaVersion !== SCHEMA_VERSION) {\n return {\n error: { kind: 'schema-version-unsupported', version: att.schemaVersion },\n ok: false,\n };\n }\n\n const expected = att.subject.contentDigest;\n const actual = `0x${sha256Hex(canonicalize(input.content))}` as const;\n if (actual !== expected) {\n return {\n error: { actualDigest: actual, expectedDigest: expected, kind: 'content-mismatch' },\n ok: false,\n };\n }\n\n let recovered: `0x${string}`;\n try {\n recovered = await recoverTypedDataAddress({\n domain: ATTESTATION_DOMAIN_V1,\n message: { claims: att.claims, schemaVersion: att.schemaVersion, subject: att.subject },\n primaryType: ATTESTATION_PRIMARY_TYPE,\n signature: att.signature,\n types: ATTESTATION_TYPES_V1,\n });\n } catch (error) {\n return {\n error: { kind: 'invalid-signature', reason: (error as Error).message },\n ok: false,\n };\n }\n\n if (recovered.toLowerCase() !== att.signerAddress.toLowerCase()) {\n return {\n error: { declared: att.signerAddress, kind: 'signer-mismatch', recovered },\n ok: false,\n };\n }\n\n return { ok: true, signerAddress: recovered };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"verify.cjs","names":["sha256","ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1","recoverTypedDataAddress","ATTESTATION_DOMAIN_V1","ATTESTATION_PRIMARY_TYPE","ATTESTATION_TYPES_V1"],"sources":["../src/version.ts","../src/core/canonicalize.ts","../src/core/sha256.ts","../src/attestation/create.ts","../src/attestation/serialize.ts","../src/attestation/verify.ts"],"sourcesContent":["// Frozen on first published attestation. Bump only by adding v2 alongside v1.\nexport const SCHEMA_VERSION = 1 as const;\nexport const CANONICAL_VERSION = 1 as const;\n","/**\n * Canonicalize an article body for attestation v1.\n *\n * FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires\n * publishing a v2 alongside (and v1 verifiers stay alive forever).\n *\n * Rules applied in order:\n * 1. Reject unpaired UTF-16 surrogates (corrupt input).\n * 2. Strip a leading BOM (U+FEFF) if present.\n * 3. Apply Unicode NFC normalization.\n * 4. Convert CRLF and stray CR to LF.\n * 5. Trim trailing whitespace and append exactly one LF.\n * 6. Encode as UTF-8 bytes.\n *\n * No markdown parsing. No per-line whitespace trim (would break \" \\n\" soft breaks).\n * No tab → space substitution (would break code blocks).\n * No interior whitespace collapsing.\n */\nexport function canonicalize(input: string): Uint8Array {\n assertValidUtf16(input);\n\n let s = input;\n if (s.charCodeAt(0) === 0xfeff) {\n s = s.slice(1);\n }\n s = s.normalize('NFC');\n s = s.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n // \\s with /u flag covers all ECMAScript whitespace, including U+00A0 and U+FEFF.\n s = `${s.replace(/\\s+$/u, '')}\\n`;\n\n return new TextEncoder().encode(s);\n}\n\n/**\n * Version of the canonicalization algorithm that produced bytes.\n * Embedded into every attestation so future verifiers can dispatch correctly.\n */\nexport { CANONICAL_VERSION as canonicalVersion } from '../version.js';\n\nexport class InvalidContentError extends Error {\n override name = 'InvalidContentError';\n}\n\nfunction assertValidUtf16(s: string): void {\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = s.charCodeAt(i + 1);\n if (!(next >= 0xdc00 && next <= 0xdfff)) {\n throw new InvalidContentError(`Unpaired high surrogate at index ${i}`);\n }\n i++;\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n throw new InvalidContentError(`Unpaired low surrogate at index ${i}`);\n }\n }\n}\n","import { sha256 } from '@noble/hashes/sha2.js';\n\n/**\n * SHA-256 of bytes, returned as bare hex (no 0x prefix).\n *\n * Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,\n * Deno, and every modern browser. This is the only hash primitive used by the\n * package, so swapping the implementation here is the single point of change.\n */\nexport function sha256Hex(bytes: Uint8Array): string {\n const out = sha256(bytes);\n let hex = '';\n for (const byte of out) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return hex;\n}\n","import { type LocalAccount } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n NO_PRIOR_ATTESTATION,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation } from './types.js';\n\nexport type CreateAttestationInput = {\n content: string;\n title: string;\n slug: string;\n locale: string;\n publishedAt: bigint | Date | number;\n revision?: number;\n priorAttestation?: `0x${string}`;\n};\n\nexport function buildAttestationMessage(input: CreateAttestationInput): AttestationMessage {\n const bytes = canonicalize(input.content);\n const digest = sha256Hex(bytes);\n\n return {\n claims: {\n priorAttestation: input.priorAttestation ?? NO_PRIOR_ATTESTATION,\n publishedAt: toUnixSeconds(input.publishedAt),\n revision: input.revision ?? 1,\n slug: input.slug,\n },\n schemaVersion: SCHEMA_VERSION,\n subject: {\n contentDigest: `0x${digest}` as const,\n locale: input.locale,\n title: input.title,\n },\n };\n}\n\nexport async function signAttestation(\n message: AttestationMessage,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n const signature = await account.signTypedData({\n domain: ATTESTATION_DOMAIN_V1,\n message,\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: ATTESTATION_TYPES_V1,\n });\n\n return {\n ...message,\n signature,\n signerAddress: account.address,\n };\n}\n\nexport async function createAttestation(\n input: CreateAttestationInput,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n return signAttestation(buildAttestationMessage(input), account);\n}\n\nfunction toUnixSeconds(value: bigint | Date | number): bigint {\n if (typeof value === 'bigint') {\n return value;\n }\n if (typeof value === 'number') {\n return BigInt(Math.floor(value));\n }\n return BigInt(Math.floor(value.getTime() / 1000));\n}\n","import { type SignedAttestation, type StoredAttestation } from './types.js';\n\nexport function toStored(att: SignedAttestation): StoredAttestation {\n return {\n claims: {\n priorAttestation: att.claims.priorAttestation,\n publishedAt: att.claims.publishedAt.toString(),\n revision: att.claims.revision,\n slug: att.claims.slug,\n },\n schemaVersion: att.schemaVersion,\n signature: att.signature,\n signerAddress: att.signerAddress,\n subject: att.subject,\n };\n}\n\nexport function fromStored(stored: StoredAttestation): SignedAttestation {\n return {\n claims: {\n priorAttestation: stored.claims.priorAttestation,\n publishedAt: BigInt(stored.claims.publishedAt),\n revision: stored.claims.revision,\n slug: stored.claims.slug,\n },\n schemaVersion: stored.schemaVersion,\n signature: stored.signature,\n signerAddress: stored.signerAddress,\n subject: stored.subject,\n };\n}\n\nexport function stringify(att: SignedAttestation): string {\n return `${JSON.stringify(toStored(att), null, 2)}\\n`;\n}\n\nexport function parse(json: string): SignedAttestation {\n const parsed = JSON.parse(json) as StoredAttestation;\n return fromStored(parsed);\n}\n","import { recoverTypedDataAddress } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation, type VerifyResult } from './types.js';\n\nexport type VerifyInput = {\n content: string;\n attestation: SignedAttestation;\n};\n\nexport async function verifyAttestation(input: VerifyInput): Promise<VerifyResult> {\n const att = input.attestation;\n\n if (att.schemaVersion !== SCHEMA_VERSION) {\n return {\n error: { kind: 'schema-version-unsupported', version: att.schemaVersion },\n ok: false,\n };\n }\n\n const expected = att.subject.contentDigest;\n const actual = `0x${sha256Hex(canonicalize(input.content))}` as const;\n if (actual !== expected) {\n return {\n error: { actualDigest: actual, expectedDigest: expected, kind: 'content-mismatch' },\n ok: false,\n };\n }\n\n let recovered: `0x${string}`;\n try {\n recovered = await recoverTypedDataAddress({\n domain: ATTESTATION_DOMAIN_V1,\n message: { claims: att.claims, schemaVersion: att.schemaVersion, subject: att.subject },\n primaryType: ATTESTATION_PRIMARY_TYPE,\n signature: att.signature,\n types: ATTESTATION_TYPES_V1,\n });\n } catch (error) {\n return {\n error: { kind: 'invalid-signature', reason: (error as Error).message },\n ok: false,\n };\n }\n\n if (recovered.toLowerCase() !== att.signerAddress.toLowerCase()) {\n return {\n error: { declared: att.signerAddress, kind: 'signer-mismatch', recovered },\n ok: false,\n };\n }\n\n return { ok: true, signerAddress: recovered };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","names":[],"sources":["../src/version.ts","../src/core/canonicalize.ts","../src/core/sha256.ts","../src/attestation/create.ts","../src/attestation/serialize.ts","../src/attestation/verify.ts"],"sourcesContent":["// Frozen on first published attestation. Bump only by adding v2 alongside v1.\nexport const SCHEMA_VERSION = 1 as const;\nexport const CANONICAL_VERSION = 1 as const;\n","/**\n * Canonicalize an article body for attestation v1.\n *\n * FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires\n * publishing a v2 alongside (and v1 verifiers stay alive forever).\n *\n * Rules applied in order:\n * 1. Reject unpaired UTF-16 surrogates (corrupt input).\n * 2. Strip a leading BOM (U+FEFF) if present.\n * 3. Apply Unicode NFC normalization.\n * 4. Convert CRLF and stray CR to LF.\n * 5. Trim trailing whitespace and append exactly one LF.\n * 6. Encode as UTF-8 bytes.\n *\n * No markdown parsing. No per-line whitespace trim (would break \" \\n\" soft breaks).\n * No tab → space substitution (would break code blocks).\n * No interior whitespace collapsing.\n */\nexport function canonicalize(input: string): Uint8Array {\n assertValidUtf16(input);\n\n let s = input;\n if (s.charCodeAt(0) === 0xfeff) {\n s = s.slice(1);\n }\n s = s.normalize('NFC');\n s = s.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n // \\s with /u flag covers all ECMAScript whitespace, including U+00A0 and U+FEFF.\n s = `${s.replace(/\\s+$/u, '')}\\n`;\n\n return new TextEncoder().encode(s);\n}\n\n/**\n * Version of the canonicalization algorithm that produced bytes.\n * Embedded into every attestation so future verifiers can dispatch correctly.\n */\nexport { CANONICAL_VERSION as canonicalVersion } from '../version.js';\n\nexport class InvalidContentError extends Error {\n override name = 'InvalidContentError';\n}\n\nfunction assertValidUtf16(s: string): void {\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = s.charCodeAt(i + 1);\n if (!(next >= 0xdc00 && next <= 0xdfff)) {\n throw new InvalidContentError(`Unpaired high surrogate at index ${i}`);\n }\n i++;\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n throw new InvalidContentError(`Unpaired low surrogate at index ${i}`);\n }\n }\n}\n","import { sha256 } from '@noble/hashes/sha2.js';\n\n/**\n * SHA-256 of bytes, returned as bare hex (no 0x prefix).\n *\n * Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,\n * Deno, and every modern browser. This is the only hash primitive used by the\n * package, so swapping the implementation here is the single point of change.\n */\nexport function sha256Hex(bytes: Uint8Array): string {\n const out = sha256(bytes);\n let hex = '';\n for (const byte of out) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return hex;\n}\n","import { type LocalAccount } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n NO_PRIOR_ATTESTATION,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation } from './types.js';\n\nexport type CreateAttestationInput = {\n content: string;\n title: string;\n slug: string;\n locale: string;\n publishedAt: bigint | Date | number;\n revision?: number;\n priorAttestation?: `0x${string}`;\n};\n\nexport function buildAttestationMessage(input: CreateAttestationInput): AttestationMessage {\n const bytes = canonicalize(input.content);\n const digest = sha256Hex(bytes);\n\n return {\n claims: {\n priorAttestation: input.priorAttestation ?? NO_PRIOR_ATTESTATION,\n publishedAt: toUnixSeconds(input.publishedAt),\n revision: input.revision ?? 1,\n slug: input.slug,\n },\n schemaVersion: SCHEMA_VERSION,\n subject: {\n contentDigest: `0x${digest}` as const,\n locale: input.locale,\n title: input.title,\n },\n };\n}\n\nexport async function signAttestation(\n message: AttestationMessage,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n const signature = await account.signTypedData({\n domain: ATTESTATION_DOMAIN_V1,\n message,\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: ATTESTATION_TYPES_V1,\n });\n\n return {\n ...message,\n signature,\n signerAddress: account.address,\n };\n}\n\nexport async function createAttestation(\n input: CreateAttestationInput,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n return signAttestation(buildAttestationMessage(input), account);\n}\n\nfunction toUnixSeconds(value: bigint | Date | number): bigint {\n if (typeof value === 'bigint') {\n return value;\n }\n if (typeof value === 'number') {\n return BigInt(Math.floor(value));\n }\n return BigInt(Math.floor(value.getTime() / 1000));\n}\n","import { type SignedAttestation, type StoredAttestation } from './types.js';\n\nexport function toStored(att: SignedAttestation): StoredAttestation {\n return {\n claims: {\n priorAttestation: att.claims.priorAttestation,\n publishedAt: att.claims.publishedAt.toString(),\n revision: att.claims.revision,\n slug: att.claims.slug,\n },\n schemaVersion: att.schemaVersion,\n signature: att.signature,\n signerAddress: att.signerAddress,\n subject: att.subject,\n };\n}\n\nexport function fromStored(stored: StoredAttestation): SignedAttestation {\n return {\n claims: {\n priorAttestation: stored.claims.priorAttestation,\n publishedAt: BigInt(stored.claims.publishedAt),\n revision: stored.claims.revision,\n slug: stored.claims.slug,\n },\n schemaVersion: stored.schemaVersion,\n signature: stored.signature,\n signerAddress: stored.signerAddress,\n subject: stored.subject,\n };\n}\n\nexport function stringify(att: SignedAttestation): string {\n return `${JSON.stringify(toStored(att), null, 2)}\\n`;\n}\n\nexport function parse(json: string): SignedAttestation {\n const parsed = JSON.parse(json) as StoredAttestation;\n return fromStored(parsed);\n}\n","import { recoverTypedDataAddress } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation, type VerifyResult } from './types.js';\n\nexport type VerifyInput = {\n content: string;\n attestation: SignedAttestation;\n};\n\nexport async function verifyAttestation(input: VerifyInput): Promise<VerifyResult> {\n const att = input.attestation;\n\n if (att.schemaVersion !== SCHEMA_VERSION) {\n return {\n error: { kind: 'schema-version-unsupported', version: att.schemaVersion },\n ok: false,\n };\n }\n\n const expected = att.subject.contentDigest;\n const actual = `0x${sha256Hex(canonicalize(input.content))}` as const;\n if (actual !== expected) {\n return {\n error: { actualDigest: actual, expectedDigest: expected, kind: 'content-mismatch' },\n ok: false,\n };\n }\n\n let recovered: `0x${string}`;\n try {\n recovered = await recoverTypedDataAddress({\n domain: ATTESTATION_DOMAIN_V1,\n message: { claims: att.claims, schemaVersion: att.schemaVersion, subject: att.subject },\n primaryType: ATTESTATION_PRIMARY_TYPE,\n signature: att.signature,\n types: ATTESTATION_TYPES_V1,\n });\n } catch (error) {\n return {\n error: { kind: 'invalid-signature', reason: (error as Error).message },\n ok: false,\n };\n }\n\n if (recovered.toLowerCase() !== att.signerAddress.toLowerCase()) {\n return {\n error: { declared: att.signerAddress, kind: 'signer-mismatch', recovered },\n ok: false,\n };\n }\n\n return { ok: true, signerAddress: recovered };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"verify.js","names":[],"sources":["../src/version.ts","../src/core/canonicalize.ts","../src/core/sha256.ts","../src/attestation/create.ts","../src/attestation/serialize.ts","../src/attestation/verify.ts"],"sourcesContent":["// Frozen on first published attestation. Bump only by adding v2 alongside v1.\nexport const SCHEMA_VERSION = 1 as const;\nexport const CANONICAL_VERSION = 1 as const;\n","/**\n * Canonicalize an article body for attestation v1.\n *\n * FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires\n * publishing a v2 alongside (and v1 verifiers stay alive forever).\n *\n * Rules applied in order:\n * 1. Reject unpaired UTF-16 surrogates (corrupt input).\n * 2. Strip a leading BOM (U+FEFF) if present.\n * 3. Apply Unicode NFC normalization.\n * 4. Convert CRLF and stray CR to LF.\n * 5. Trim trailing whitespace and append exactly one LF.\n * 6. Encode as UTF-8 bytes.\n *\n * No markdown parsing. No per-line whitespace trim (would break \" \\n\" soft breaks).\n * No tab → space substitution (would break code blocks).\n * No interior whitespace collapsing.\n */\nexport function canonicalize(input: string): Uint8Array {\n assertValidUtf16(input);\n\n let s = input;\n if (s.charCodeAt(0) === 0xfeff) {\n s = s.slice(1);\n }\n s = s.normalize('NFC');\n s = s.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n // \\s with /u flag covers all ECMAScript whitespace, including U+00A0 and U+FEFF.\n s = `${s.replace(/\\s+$/u, '')}\\n`;\n\n return new TextEncoder().encode(s);\n}\n\n/**\n * Version of the canonicalization algorithm that produced bytes.\n * Embedded into every attestation so future verifiers can dispatch correctly.\n */\nexport { CANONICAL_VERSION as canonicalVersion } from '../version.js';\n\nexport class InvalidContentError extends Error {\n override name = 'InvalidContentError';\n}\n\nfunction assertValidUtf16(s: string): void {\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = s.charCodeAt(i + 1);\n if (!(next >= 0xdc00 && next <= 0xdfff)) {\n throw new InvalidContentError(`Unpaired high surrogate at index ${i}`);\n }\n i++;\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n throw new InvalidContentError(`Unpaired low surrogate at index ${i}`);\n }\n }\n}\n","import { sha256 } from '@noble/hashes/sha2.js';\n\n/**\n * SHA-256 of bytes, returned as bare hex (no 0x prefix).\n *\n * Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,\n * Deno, and every modern browser. This is the only hash primitive used by the\n * package, so swapping the implementation here is the single point of change.\n */\nexport function sha256Hex(bytes: Uint8Array): string {\n const out = sha256(bytes);\n let hex = '';\n for (const byte of out) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return hex;\n}\n","import { type LocalAccount } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n NO_PRIOR_ATTESTATION,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation } from './types.js';\n\nexport type CreateAttestationInput = {\n content: string;\n title: string;\n slug: string;\n locale: string;\n publishedAt: bigint | Date | number;\n revision?: number;\n priorAttestation?: `0x${string}`;\n};\n\nexport function buildAttestationMessage(input: CreateAttestationInput): AttestationMessage {\n const bytes = canonicalize(input.content);\n const digest = sha256Hex(bytes);\n\n return {\n claims: {\n priorAttestation: input.priorAttestation ?? NO_PRIOR_ATTESTATION,\n publishedAt: toUnixSeconds(input.publishedAt),\n revision: input.revision ?? 1,\n slug: input.slug,\n },\n schemaVersion: SCHEMA_VERSION,\n subject: {\n contentDigest: `0x${digest}` as const,\n locale: input.locale,\n title: input.title,\n },\n };\n}\n\nexport async function signAttestation(\n message: AttestationMessage,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n const signature = await account.signTypedData({\n domain: ATTESTATION_DOMAIN_V1,\n message,\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: ATTESTATION_TYPES_V1,\n });\n\n return {\n ...message,\n signature,\n signerAddress: account.address,\n };\n}\n\nexport async function createAttestation(\n input: CreateAttestationInput,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n return signAttestation(buildAttestationMessage(input), account);\n}\n\nfunction toUnixSeconds(value: bigint | Date | number): bigint {\n if (typeof value === 'bigint') {\n return value;\n }\n if (typeof value === 'number') {\n return BigInt(Math.floor(value));\n }\n return BigInt(Math.floor(value.getTime() / 1000));\n}\n","import { type SignedAttestation, type StoredAttestation } from './types.js';\n\nexport function toStored(att: SignedAttestation): StoredAttestation {\n return {\n claims: {\n priorAttestation: att.claims.priorAttestation,\n publishedAt: att.claims.publishedAt.toString(),\n revision: att.claims.revision,\n slug: att.claims.slug,\n },\n schemaVersion: att.schemaVersion,\n signature: att.signature,\n signerAddress: att.signerAddress,\n subject: att.subject,\n };\n}\n\nexport function fromStored(stored: StoredAttestation): SignedAttestation {\n return {\n claims: {\n priorAttestation: stored.claims.priorAttestation,\n publishedAt: BigInt(stored.claims.publishedAt),\n revision: stored.claims.revision,\n slug: stored.claims.slug,\n },\n schemaVersion: stored.schemaVersion,\n signature: stored.signature,\n signerAddress: stored.signerAddress,\n subject: stored.subject,\n };\n}\n\nexport function stringify(att: SignedAttestation): string {\n return `${JSON.stringify(toStored(att), null, 2)}\\n`;\n}\n\nexport function parse(json: string): SignedAttestation {\n const parsed = JSON.parse(json) as StoredAttestation;\n return fromStored(parsed);\n}\n","import { recoverTypedDataAddress } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation, type VerifyResult } from './types.js';\n\nexport type VerifyInput = {\n content: string;\n attestation: SignedAttestation;\n};\n\nexport async function verifyAttestation(input: VerifyInput): Promise<VerifyResult> {\n const att = input.attestation;\n\n if (att.schemaVersion !== SCHEMA_VERSION) {\n return {\n error: { kind: 'schema-version-unsupported', version: att.schemaVersion },\n ok: false,\n };\n }\n\n const expected = att.subject.contentDigest;\n const actual = `0x${sha256Hex(canonicalize(input.content))}` as const;\n if (actual !== expected) {\n return {\n error: { actualDigest: actual, expectedDigest: expected, kind: 'content-mismatch' },\n ok: false,\n };\n }\n\n let recovered: `0x${string}`;\n try {\n recovered = await recoverTypedDataAddress({\n domain: ATTESTATION_DOMAIN_V1,\n message: { claims: att.claims, schemaVersion: att.schemaVersion, subject: att.subject },\n primaryType: ATTESTATION_PRIMARY_TYPE,\n signature: att.signature,\n types: ATTESTATION_TYPES_V1,\n });\n } catch (error) {\n return {\n error: { kind: 'invalid-signature', reason: (error as Error).message },\n ok: false,\n };\n }\n\n if (recovered.toLowerCase() !== att.signerAddress.toLowerCase()) {\n return {\n error: { declared: att.signerAddress, kind: 'signer-mismatch', recovered },\n ok: false,\n };\n }\n\n return { ok: true, signerAddress: recovered };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/attestation",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Cryptographic attestation for articles — EIP-712 signature anchored to Bitcoin via OpenTimestamps.",
5
5
  "license": "MIT",
6
6
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
@@ -49,8 +49,8 @@
49
49
  "viem": "^2.54.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@jterrazz/test": "^8.0.0",
53
- "@jterrazz/typescript": "^6.1.0",
52
+ "@jterrazz/test": "^14.1.0",
53
+ "@jterrazz/typescript": "^9.0.0",
54
54
  "@types/node": "^24.13.2",
55
55
  "tsdown": "^0.22.3",
56
56
  "vitest": "^4.1.9"