@motebit/state-export-client 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Recursive verification for v1.1 inner signed receipts.
3
+ *
4
+ * The relay-assembled execution-ledger reconstruction at
5
+ * `/api/v1/execution/:motebitId/:goalId` carries the v1.1 optional
6
+ * `signed_receipts: string[]` field — byte-identical canonical-JSON of
7
+ * each delegated motebit's signed `ExecutionReceipt`, sourced from the
8
+ * relay's `relay_receipts.receipt_json` archive (per
9
+ * `services/relay/CLAUDE.md` Rule 11). The wire shape and rationale
10
+ * live in `spec/execution-ledger-v1.md` §4.3 (Inner Signed Receipts —
11
+ * v1.1 additive).
12
+ *
13
+ * Without recursive verification, the v1.1 wire change is invisible
14
+ * truth: the verifier sees the field but does nothing with it. This
15
+ * module closes the consumer-side asymmetry. Each receipt is parsed
16
+ * back into its `ExecutionReceipt` shape, passed to
17
+ * `verifyReceipt` from `@motebit/crypto`, and its Ed25519 signature
18
+ * checked against the embedded `public_key` independently of the
19
+ * relay. Multi-hop delegation chains are walked recursively.
20
+ *
21
+ * Doctrine: `docs/doctrine/nist-alignment.md` §8 "Inner-receipt
22
+ * verification closed"; `docs/doctrine/self-attesting-system.md`.
23
+ */
24
+ import { EXECUTION_LEDGER_SPEC_V1_1 } from "@motebit/protocol";
25
+ import { verifyReceipt } from "@motebit/crypto";
26
+ /**
27
+ * Verify the inner signed receipts inside an execution-ledger response
28
+ * body. Idempotent + side-effect-free; no network calls beyond what
29
+ * `verifyReceipt` performs (which itself is offline — every receipt
30
+ * carries its own `public_key`).
31
+ *
32
+ * Returns `{ applicable: false }` for v1.0 bodies, bodies without
33
+ * `signed_receipts`, or bodies that aren't execution-ledger shape.
34
+ * Returns `{ applicable: true, allValid, ... }` with per-receipt
35
+ * outcomes for v1.1 bodies that carry the field.
36
+ */
37
+ export async function verifyInnerSignedReceipts(body) {
38
+ if (!isV1_1ExecutionLedger(body)) {
39
+ return {
40
+ applicable: false,
41
+ allValid: false,
42
+ verifiedCount: 0,
43
+ totalCount: 0,
44
+ results: [],
45
+ };
46
+ }
47
+ const signedReceipts = body.signed_receipts;
48
+ if (signedReceipts === undefined || signedReceipts.length === 0) {
49
+ return {
50
+ applicable: false,
51
+ allValid: false,
52
+ verifiedCount: 0,
53
+ totalCount: 0,
54
+ results: [],
55
+ };
56
+ }
57
+ const results = [];
58
+ for (const entry of signedReceipts) {
59
+ results.push(await verifyOneInner(entry));
60
+ }
61
+ const verifiedCount = results.filter((r) => r.valid).length;
62
+ return {
63
+ applicable: true,
64
+ allValid: verifiedCount === results.length,
65
+ verifiedCount,
66
+ totalCount: results.length,
67
+ results,
68
+ };
69
+ }
70
+ function isV1_1ExecutionLedger(body) {
71
+ if (typeof body !== "object" || body === null)
72
+ return false;
73
+ const b = body;
74
+ if (b.spec !== EXECUTION_LEDGER_SPEC_V1_1)
75
+ return false;
76
+ return b.signed_receipts === undefined || Array.isArray(b.signed_receipts);
77
+ }
78
+ async function verifyOneInner(entryJson) {
79
+ let receipt;
80
+ try {
81
+ receipt = JSON.parse(entryJson);
82
+ }
83
+ catch (err) {
84
+ return {
85
+ taskId: "<unparseable>",
86
+ motebitId: "<unparseable>",
87
+ valid: false,
88
+ reason: "malformed_json",
89
+ detail: err instanceof Error ? err.message : String(err),
90
+ };
91
+ }
92
+ const result = await verifyReceipt(receipt);
93
+ if (result.valid) {
94
+ return {
95
+ taskId: receipt.task_id,
96
+ motebitId: String(receipt.motebit_id),
97
+ ...(result.signer !== undefined && { signerDid: result.signer }),
98
+ valid: true,
99
+ ...(result.delegations !== undefined && result.delegations.length > 0
100
+ ? { delegations: result.delegations.map(toInnerShape) }
101
+ : {}),
102
+ };
103
+ }
104
+ // Map the crypto-layer ReceiptVerifyResult into the consumer-facing
105
+ // typed-failure shape. We surface the cheapest cause: missing key
106
+ // before signature failure, delegation failures last.
107
+ const errs = result.errors ?? [];
108
+ let reason = "unknown";
109
+ let detail;
110
+ if (errs.some((e) => e.message.includes("No embedded public_key"))) {
111
+ reason = "missing_public_key";
112
+ detail = errs.find((e) => e.message.includes("No embedded public_key"))?.message;
113
+ }
114
+ else if (errs.some((e) => e.path === "delegation_receipts")) {
115
+ reason = "delegation_failed";
116
+ detail = errs.find((e) => e.path === "delegation_receipts")?.message;
117
+ }
118
+ else if (errs.length > 0) {
119
+ reason = "signature_invalid";
120
+ detail = errs[0]?.message;
121
+ }
122
+ return {
123
+ taskId: receipt.task_id,
124
+ motebitId: String(receipt.motebit_id),
125
+ ...(result.signer !== undefined && { signerDid: result.signer }),
126
+ valid: false,
127
+ reason,
128
+ ...(detail !== undefined && { detail }),
129
+ ...(result.delegations !== undefined && result.delegations.length > 0
130
+ ? { delegations: result.delegations.map(toInnerShape) }
131
+ : {}),
132
+ };
133
+ }
134
+ // Lift the crypto-layer ReceiptVerifyResult shape into the consumer
135
+ // shape so callers don't need to import @motebit/crypto types.
136
+ function toInnerShape(r) {
137
+ const errs = r.errors ?? [];
138
+ let reason;
139
+ if (!r.valid) {
140
+ if (errs.some((e) => e.message.includes("No embedded public_key")))
141
+ reason = "missing_public_key";
142
+ else if (errs.some((e) => e.path === "delegation_receipts"))
143
+ reason = "delegation_failed";
144
+ else if (errs.length > 0)
145
+ reason = "signature_invalid";
146
+ else
147
+ reason = "unknown";
148
+ }
149
+ return {
150
+ taskId: r.receipt?.task_id ?? "<unknown>",
151
+ motebitId: String(r.receipt?.motebit_id ?? "<unknown>"),
152
+ ...(r.signer !== undefined && { signerDid: r.signer }),
153
+ valid: r.valid,
154
+ ...(reason !== undefined && { reason }),
155
+ ...(errs[0]?.message !== undefined && !r.valid && { detail: errs[0].message }),
156
+ ...(r.delegations !== undefined && r.delegations.length > 0
157
+ ? { delegations: r.delegations.map(toInnerShape) }
158
+ : {}),
159
+ };
160
+ }
161
+ //# sourceMappingURL=inner-receipts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inner-receipts.js","sourceRoot":"","sources":["../src/inner-receipts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Onchain cross-check for the operator-transparency declaration.
3
+ *
4
+ * Trust-on-first-use (TOFU) bootstrap is the weakest link in the
5
+ * offline-after-bootstrap verification story — the first fetch of
6
+ * `/.well-known/motebit-transparency.json` trusts HTTPS + DNS + CAs.
7
+ * A DNS hijack, malicious ISP, or compromised CA can substitute a
8
+ * different declaration with the attacker's key embedded (the
9
+ * self-signature still verifies — against the attacker's key).
10
+ *
11
+ * Closure: anchor the declaration's hash to Solana via the Memo
12
+ * program at deploy/declare time. A verifier with the relay's pinned
13
+ * Solana address (out-of-band trust root, like Apple's App Attest root
14
+ * cert) cross-checks `sha256(declaration)` against memos at that
15
+ * address. Mismatch — or no memo at all — surfaces a typed reason.
16
+ *
17
+ * No SDK dep. Solana JSON-RPC is plain HTTP-JSON; this module uses
18
+ * `fetch` directly to keep `@motebit/state-export-client` browser-safe
19
+ * and dep-thin (no `@solana/web3.js` pulled into web bundles).
20
+ *
21
+ * Doctrine: `docs/doctrine/operator-transparency.md` § Stage 2 onchain
22
+ * anchor; `docs/doctrine/nist-alignment.md` §8 "savant gap closure".
23
+ */
24
+ import type { SignedTransparencyDeclaration } from "./transparency-anchor.js";
25
+ export interface OnchainAnchorLookupOptions {
26
+ /**
27
+ * Solana JSON-RPC endpoint URL. Defaults to mainnet-beta — production
28
+ * verifiers SHOULD pin a known-good RPC (Helius / Triton / self-hosted)
29
+ * to avoid the same kind of supply-chain risk the anchor exists to
30
+ * close. Tests pass a fixture URL backed by a mock fetch.
31
+ */
32
+ readonly rpcUrl?: string;
33
+ /**
34
+ * Inject the fetch implementation. Defaults to global `fetch`. Tests
35
+ * pass a mock; integrators with custom transport pass a wrapper.
36
+ */
37
+ readonly fetch?: typeof globalThis.fetch;
38
+ /**
39
+ * Max signatures to scan at the anchor address. Memos are append-only;
40
+ * the latest one is the current declaration anchor. Older signatures
41
+ * are historical declarations the operator has cycled through.
42
+ * Default 50 — covers typical operator cadence (declarations change
43
+ * on doctrine update or key rotation, not frequently).
44
+ */
45
+ readonly maxSignatures?: number;
46
+ }
47
+ /** Verification outcome with a structured failure reason for audit logging. */
48
+ export type OnchainAnchorResult = {
49
+ readonly ok: true;
50
+ readonly txHash: string;
51
+ readonly anchoredHashHex: string;
52
+ readonly anchorAddress: string;
53
+ } | {
54
+ readonly ok: false;
55
+ readonly reason: OnchainAnchorFailureReason;
56
+ readonly detail?: string;
57
+ };
58
+ export type OnchainAnchorFailureReason = "rpc_failed" | "no_anchor_found" | "anchor_hash_mismatch" | "malformed_memo";
59
+ /**
60
+ * Look up the latest onchain anchor for a transparency declaration.
61
+ * Returns a typed result — never throws on verification failure; HTTP
62
+ * errors surface as `rpc_failed`.
63
+ *
64
+ * Algorithm:
65
+ *
66
+ * 1. `getSignaturesForAddress(anchorAddress)` returns recent signatures
67
+ * with their memo field populated (the JSON-RPC returns the memo
68
+ * inline because the Memo program emits the data in the tx log).
69
+ * 2. Filter to signatures whose memo starts with the canonical
70
+ * `motebit:transparency:v1:` prefix.
71
+ * 3. Pick the most recent (signatures are returned newest-first).
72
+ * 4. Parse the hash out of the memo (`motebit:transparency:v1:<hash>`).
73
+ * 5. Compare against `expectedHashHex`. Equality → anchored; mismatch
74
+ * → tampering; no match in scan → never anchored.
75
+ *
76
+ * The pinned `anchorAddress` is the trust root. It MUST be obtained
77
+ * out-of-band (published in the motebit canonical config, the docs site,
78
+ * or a known motebit-org keyring) — passing the value from the
79
+ * declaration itself would be circular trust.
80
+ */
81
+ export declare function lookupTransparencyAnchor(anchorAddress: string, expectedHashHex: string, options?: OnchainAnchorLookupOptions): Promise<OnchainAnchorResult>;
82
+ /**
83
+ * Convenience: cross-check a transparency declaration against an
84
+ * onchain anchor. Combines hash extraction from the declaration with
85
+ * `lookupTransparencyAnchor`. Returns the same typed result.
86
+ *
87
+ * The verifier expected to call this AFTER `verifyTransparencyDeclaration`
88
+ * has confirmed the self-signature — anchor verification adds a second
89
+ * trust channel on top of the self-signature check; it doesn't replace
90
+ * the self-signature.
91
+ */
92
+ export declare function verifyDeclarationOnchainAnchor(declaration: SignedTransparencyDeclaration, anchorAddress: string, options?: OnchainAnchorLookupOptions): Promise<OnchainAnchorResult>;
93
+ //# sourceMappingURL=onchain-anchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onchain-anchor.d.ts","sourceRoot":"","sources":["../src/onchain-anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAK9E,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,+EAA+E;AAC/E,MAAM,MAAM,mBAAmB,GAC3B;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEN,MAAM,MAAM,0BAA0B,GAClC,YAAY,GACZ,iBAAiB,GACjB,sBAAsB,GACtB,gBAAgB,CAAC;AAiBrB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,wBAAwB,CAC5C,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,mBAAmB,CAAC,CAkF9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,8BAA8B,CAClD,WAAW,EAAE,6BAA6B,EAC1C,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,mBAAmB,CAAC,CAE9B"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Onchain cross-check for the operator-transparency declaration.
3
+ *
4
+ * Trust-on-first-use (TOFU) bootstrap is the weakest link in the
5
+ * offline-after-bootstrap verification story — the first fetch of
6
+ * `/.well-known/motebit-transparency.json` trusts HTTPS + DNS + CAs.
7
+ * A DNS hijack, malicious ISP, or compromised CA can substitute a
8
+ * different declaration with the attacker's key embedded (the
9
+ * self-signature still verifies — against the attacker's key).
10
+ *
11
+ * Closure: anchor the declaration's hash to Solana via the Memo
12
+ * program at deploy/declare time. A verifier with the relay's pinned
13
+ * Solana address (out-of-band trust root, like Apple's App Attest root
14
+ * cert) cross-checks `sha256(declaration)` against memos at that
15
+ * address. Mismatch — or no memo at all — surfaces a typed reason.
16
+ *
17
+ * No SDK dep. Solana JSON-RPC is plain HTTP-JSON; this module uses
18
+ * `fetch` directly to keep `@motebit/state-export-client` browser-safe
19
+ * and dep-thin (no `@solana/web3.js` pulled into web bundles).
20
+ *
21
+ * Doctrine: `docs/doctrine/operator-transparency.md` § Stage 2 onchain
22
+ * anchor; `docs/doctrine/nist-alignment.md` §8 "savant gap closure".
23
+ */
24
+ /** Canonical memo prefix the relay emits for transparency anchors. */
25
+ const TRANSPARENCY_MEMO_PREFIX = "motebit:transparency:v1:";
26
+ /**
27
+ * Look up the latest onchain anchor for a transparency declaration.
28
+ * Returns a typed result — never throws on verification failure; HTTP
29
+ * errors surface as `rpc_failed`.
30
+ *
31
+ * Algorithm:
32
+ *
33
+ * 1. `getSignaturesForAddress(anchorAddress)` returns recent signatures
34
+ * with their memo field populated (the JSON-RPC returns the memo
35
+ * inline because the Memo program emits the data in the tx log).
36
+ * 2. Filter to signatures whose memo starts with the canonical
37
+ * `motebit:transparency:v1:` prefix.
38
+ * 3. Pick the most recent (signatures are returned newest-first).
39
+ * 4. Parse the hash out of the memo (`motebit:transparency:v1:<hash>`).
40
+ * 5. Compare against `expectedHashHex`. Equality → anchored; mismatch
41
+ * → tampering; no match in scan → never anchored.
42
+ *
43
+ * The pinned `anchorAddress` is the trust root. It MUST be obtained
44
+ * out-of-band (published in the motebit canonical config, the docs site,
45
+ * or a known motebit-org keyring) — passing the value from the
46
+ * declaration itself would be circular trust.
47
+ */
48
+ export async function lookupTransparencyAnchor(anchorAddress, expectedHashHex, options = {}) {
49
+ const rpcUrl = options.rpcUrl ?? "https://api.mainnet-beta.solana.com";
50
+ const fetchImpl = options.fetch ?? globalThis.fetch;
51
+ const limit = options.maxSignatures ?? 50;
52
+ let signatures;
53
+ try {
54
+ const res = await fetchImpl(rpcUrl, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({
58
+ jsonrpc: "2.0",
59
+ id: 1,
60
+ method: "getSignaturesForAddress",
61
+ params: [anchorAddress, { limit }],
62
+ }),
63
+ });
64
+ if (!res.ok) {
65
+ return { ok: false, reason: "rpc_failed", detail: `HTTP ${res.status}` };
66
+ }
67
+ const body = (await res.json());
68
+ if (body.error !== undefined) {
69
+ return { ok: false, reason: "rpc_failed", detail: body.error.message };
70
+ }
71
+ signatures = body.result ?? [];
72
+ }
73
+ catch (err) {
74
+ return {
75
+ ok: false,
76
+ reason: "rpc_failed",
77
+ detail: err instanceof Error ? err.message : String(err),
78
+ };
79
+ }
80
+ // Newest-first per Solana RPC convention. Find the first valid
81
+ // transparency anchor memo. The memo field appears in the signature
82
+ // result when the tx includes the Memo program — solana-rpc parses
83
+ // and surfaces the data inline.
84
+ for (const sig of signatures) {
85
+ if (sig.err !== null)
86
+ continue; // skip failed txs
87
+ if (sig.memo == null)
88
+ continue;
89
+ // Solana RPC formats memo as `[<size> (len <bytes>)] <utf-8>`. The
90
+ // leading bracket-prefix is metadata; the actual memo bytes follow.
91
+ // Match the canonical prefix anywhere in the formatted string —
92
+ // robust to format variation across RPC versions.
93
+ const idx = sig.memo.indexOf(TRANSPARENCY_MEMO_PREFIX);
94
+ if (idx === -1)
95
+ continue;
96
+ const after = sig.memo.slice(idx + TRANSPARENCY_MEMO_PREFIX.length);
97
+ // The hash continues until the first non-hex character (end of string,
98
+ // closing bracket from RPC formatting, whitespace, etc.).
99
+ const hashMatch = after.match(/^([0-9a-fA-F]{64})/);
100
+ if (hashMatch == null) {
101
+ return {
102
+ ok: false,
103
+ reason: "malformed_memo",
104
+ detail: `memo prefix matched but hash slot is not 64 hex chars: "${after.slice(0, 80)}"`,
105
+ };
106
+ }
107
+ const anchoredHashHex = hashMatch[1].toLowerCase();
108
+ if (anchoredHashHex !== expectedHashHex.toLowerCase()) {
109
+ return {
110
+ ok: false,
111
+ reason: "anchor_hash_mismatch",
112
+ detail: `expected ${expectedHashHex.toLowerCase()}, got ${anchoredHashHex}`,
113
+ };
114
+ }
115
+ return {
116
+ ok: true,
117
+ txHash: sig.signature,
118
+ anchoredHashHex,
119
+ anchorAddress,
120
+ };
121
+ }
122
+ return {
123
+ ok: false,
124
+ reason: "no_anchor_found",
125
+ detail: `scanned ${signatures.length} signature(s) at ${anchorAddress}, none matched ${TRANSPARENCY_MEMO_PREFIX}<hash>`,
126
+ };
127
+ }
128
+ /**
129
+ * Convenience: cross-check a transparency declaration against an
130
+ * onchain anchor. Combines hash extraction from the declaration with
131
+ * `lookupTransparencyAnchor`. Returns the same typed result.
132
+ *
133
+ * The verifier expected to call this AFTER `verifyTransparencyDeclaration`
134
+ * has confirmed the self-signature — anchor verification adds a second
135
+ * trust channel on top of the self-signature check; it doesn't replace
136
+ * the self-signature.
137
+ */
138
+ export async function verifyDeclarationOnchainAnchor(declaration, anchorAddress, options = {}) {
139
+ return lookupTransparencyAnchor(anchorAddress, declaration.hash, options);
140
+ }
141
+ //# sourceMappingURL=onchain-anchor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onchain-anchor.js","sourceRoot":"","sources":["../src/onchain-anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,sEAAsE;AACtE,MAAM,wBAAwB,GAAG,0BAA0B,CAAC;AA4D5D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,aAAqB,EACrB,eAAuB,EACvB,UAAsC,EAAE;IAExC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,qCAAqC,CAAC;IACvE,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACpD,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;IAE1C,IAAI,UAA2B,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE;YAClC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,EAAE,EAAE,CAAC;gBACL,MAAM,EAAE,yBAAyB;gBACjC,MAAM,EAAE,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC;aACnC,CAAC;SACH,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;QAC3E,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAiC,CAAC;QAChE,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACzE,CAAC;QACD,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,YAAY;YACpB,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACzD,CAAC;IACJ,CAAC;IAED,+DAA+D;IAC/D,oEAAoE;IACpE,mEAAmE;IACnE,gCAAgC;IAChC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI;YAAE,SAAS,CAAC,kBAAkB;QAClD,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI;YAAE,SAAS;QAE/B,mEAAmE;QACnE,oEAAoE;QACpE,gEAAgE;QAChE,kDAAkD;QAClD,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;QACvD,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,SAAS;QAEzB,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;QACpE,uEAAuE;QACvE,0DAA0D;QAC1D,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACpD,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;YACtB,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,gBAAgB;gBACxB,MAAM,EAAE,2DAA2D,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG;aACzF,CAAC;QACJ,CAAC;QACD,MAAM,eAAe,GAAG,SAAS,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAC;QAEpD,IAAI,eAAe,KAAK,eAAe,CAAC,WAAW,EAAE,EAAE,CAAC;YACtD,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,sBAAsB;gBAC9B,MAAM,EAAE,YAAY,eAAe,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE;aAC5E,CAAC;QACJ,CAAC;QAED,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,GAAG,CAAC,SAAS;YACrB,eAAe;YACf,aAAa;SACd,CAAC;IACJ,CAAC;IAED,OAAO;QACL,EAAE,EAAE,KAAK;QACT,MAAM,EAAE,iBAAiB;QACzB,MAAM,EAAE,WAAW,UAAU,CAAC,MAAM,oBAAoB,aAAa,kBAAkB,wBAAwB,QAAQ;KACxH,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,WAA0C,EAC1C,aAAqB,EACrB,UAAsC,EAAE;IAExC,OAAO,wBAAwB,CAAC,aAAa,EAAE,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5E,CAAC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Trust-anchor discovery from `/.well-known/motebit-transparency.json`.
3
+ *
4
+ * The operator-transparency declaration (`docs/doctrine/operator-transparency.md`)
5
+ * is a self-signed JSON artifact: the relay's Ed25519 public key is
6
+ * embedded inside the signed payload, and the payload's signature verifies
7
+ * against that same key. Trust-on-first-use (TOFU): a verifier that does
8
+ * not yet know the relay's key fetches the declaration once, verifies its
9
+ * self-signature, and caches the key. From then on every state-export
10
+ * `X-Motebit-Content-Manifest` header is verified against the cached key
11
+ * offline — no further relay contact at verify time.
12
+ *
13
+ * Why this is sufficient (not circular):
14
+ * The declaration's signature commits the relay to the key it carries.
15
+ * An attacker who substitutes a different key must also re-sign the
16
+ * declaration; doing so produces a different declaration that any
17
+ * holder of a prior (signed) declaration can detect by comparing keys
18
+ * or by walking the onchain anchor chain once stage 2 ships. The
19
+ * disappearance test still applies: the declaration is durable across
20
+ * operator vanishings; the key it commits to remains the anchor.
21
+ *
22
+ * Doctrine: `docs/doctrine/operator-transparency.md`, `docs/doctrine/nist-alignment.md` §8.
23
+ */
24
+ import type { SignedTransparencyDeclaration } from "@motebit/protocol";
25
+ export type { SignedTransparencyDeclaration } from "@motebit/protocol";
26
+ /**
27
+ * The pinned trust anchor — what a verifier carries forward after a
28
+ * successful TOFU bootstrap. Subsequent state-export manifests are
29
+ * checked against `relayPublicKey`.
30
+ */
31
+ export interface TransparencyAnchor {
32
+ /** 32-byte Ed25519 public key — the canonical signer for this operator. */
33
+ readonly relayPublicKey: Uint8Array;
34
+ /** Hex form, for `motebit-verify --producer-key` pinning + log display. */
35
+ readonly relayPublicKeyHex: string;
36
+ /** Relay motebit ID from the declaration. */
37
+ readonly relayId: string;
38
+ /** ISO timestamp of the declaration. */
39
+ readonly declaredAt: number;
40
+ }
41
+ export interface FetchTransparencyAnchorOptions {
42
+ /**
43
+ * Override the default endpoint path. Production callers leave this
44
+ * unset; tests pass a fixture path. The default mirrors the
45
+ * well-known URI defined in `docs/doctrine/operator-transparency.md`.
46
+ */
47
+ readonly path?: string;
48
+ /**
49
+ * Inject the fetch implementation. Defaults to global `fetch`.
50
+ * Tests pass a mock; integrators with custom transport (auth proxy,
51
+ * tunneling) pass a wrapper.
52
+ */
53
+ readonly fetch?: typeof globalThis.fetch;
54
+ /** Abort signal for the network request. */
55
+ readonly signal?: AbortSignal;
56
+ }
57
+ /** Verification outcome with a structured failure reason for audit logging. */
58
+ export type TransparencyAnchorResult = {
59
+ readonly ok: true;
60
+ readonly anchor: TransparencyAnchor;
61
+ } | {
62
+ readonly ok: false;
63
+ readonly reason: TransparencyAnchorFailureReason;
64
+ readonly detail?: string;
65
+ };
66
+ export type TransparencyAnchorFailureReason = "fetch_failed" | "malformed_declaration" | "hash_mismatch" | "malformed_public_key" | "malformed_signature" | "signature_invalid" | "unsupported_suite";
67
+ /**
68
+ * Fetch the operator-transparency declaration and verify its
69
+ * self-signature. Returns the pinned `TransparencyAnchor` on success,
70
+ * or a structured failure reason. Fail-closed — every rejection has a
71
+ * typed reason, no thrown exceptions for verification failures.
72
+ *
73
+ * The relay's identity key is embedded in the declaration as
74
+ * `relay_public_key` (hex) and the signature is over
75
+ * `canonicalJson({spec, declared_at, relay_id, relay_public_key, content})`.
76
+ * Verification recomputes the hash, then checks the signature against
77
+ * the declared key via `verifyBySuite`.
78
+ */
79
+ export declare function fetchTransparencyAnchor(baseUrl: string, options?: FetchTransparencyAnchorOptions): Promise<TransparencyAnchorResult>;
80
+ /**
81
+ * Verify an already-fetched signed transparency declaration. Exposed
82
+ * separately so a verifier with a cached declaration (e.g. captured
83
+ * to a file by an auditor) can re-verify without a network round-trip.
84
+ */
85
+ export declare function verifyTransparencyDeclaration(declaration: SignedTransparencyDeclaration): Promise<TransparencyAnchorResult>;
86
+ //# sourceMappingURL=transparency-anchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transparency-anchor.d.ts","sourceRoot":"","sources":["../src/transparency-anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAMvE,YAAY,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAEvE;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,+EAA+E;AAC/E,MAAM,MAAM,wBAAwB,GAChC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC1D;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEN,MAAM,MAAM,+BAA+B,GACvC,cAAc,GACd,uBAAuB,GACvB,eAAe,GACf,sBAAsB,GACtB,qBAAqB,GACrB,mBAAmB,GACnB,mBAAmB,CAAC;AAExB;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,wBAAwB,CAAC,CAyBnC;AAED;;;;GAIG;AACH,wBAAsB,6BAA6B,CACjD,WAAW,EAAE,6BAA6B,GACzC,OAAO,CAAC,wBAAwB,CAAC,CA8EnC"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Trust-anchor discovery from `/.well-known/motebit-transparency.json`.
3
+ *
4
+ * The operator-transparency declaration (`docs/doctrine/operator-transparency.md`)
5
+ * is a self-signed JSON artifact: the relay's Ed25519 public key is
6
+ * embedded inside the signed payload, and the payload's signature verifies
7
+ * against that same key. Trust-on-first-use (TOFU): a verifier that does
8
+ * not yet know the relay's key fetches the declaration once, verifies its
9
+ * self-signature, and caches the key. From then on every state-export
10
+ * `X-Motebit-Content-Manifest` header is verified against the cached key
11
+ * offline — no further relay contact at verify time.
12
+ *
13
+ * Why this is sufficient (not circular):
14
+ * The declaration's signature commits the relay to the key it carries.
15
+ * An attacker who substitutes a different key must also re-sign the
16
+ * declaration; doing so produces a different declaration that any
17
+ * holder of a prior (signed) declaration can detect by comparing keys
18
+ * or by walking the onchain anchor chain once stage 2 ships. The
19
+ * disappearance test still applies: the declaration is durable across
20
+ * operator vanishings; the key it commits to remains the anchor.
21
+ *
22
+ * Doctrine: `docs/doctrine/operator-transparency.md`, `docs/doctrine/nist-alignment.md` §8.
23
+ */
24
+ import { canonicalJson, hexToBytes, sha256, bytesToHex, verifyBySuite } from "@motebit/crypto";
25
+ /**
26
+ * Fetch the operator-transparency declaration and verify its
27
+ * self-signature. Returns the pinned `TransparencyAnchor` on success,
28
+ * or a structured failure reason. Fail-closed — every rejection has a
29
+ * typed reason, no thrown exceptions for verification failures.
30
+ *
31
+ * The relay's identity key is embedded in the declaration as
32
+ * `relay_public_key` (hex) and the signature is over
33
+ * `canonicalJson({spec, declared_at, relay_id, relay_public_key, content})`.
34
+ * Verification recomputes the hash, then checks the signature against
35
+ * the declared key via `verifyBySuite`.
36
+ */
37
+ export async function fetchTransparencyAnchor(baseUrl, options = {}) {
38
+ const path = options.path ?? "/.well-known/motebit-transparency.json";
39
+ const url = `${baseUrl.replace(/\/$/, "")}${path}`;
40
+ const fetchImpl = options.fetch ?? globalThis.fetch;
41
+ let declaration;
42
+ try {
43
+ const res = await fetchImpl(url, { signal: options.signal });
44
+ if (!res.ok) {
45
+ return {
46
+ ok: false,
47
+ reason: "fetch_failed",
48
+ detail: `HTTP ${res.status} ${res.statusText}`,
49
+ };
50
+ }
51
+ declaration = (await res.json());
52
+ }
53
+ catch (err) {
54
+ return {
55
+ ok: false,
56
+ reason: "fetch_failed",
57
+ detail: err instanceof Error ? err.message : String(err),
58
+ };
59
+ }
60
+ return verifyTransparencyDeclaration(declaration);
61
+ }
62
+ /**
63
+ * Verify an already-fetched signed transparency declaration. Exposed
64
+ * separately so a verifier with a cached declaration (e.g. captured
65
+ * to a file by an auditor) can re-verify without a network round-trip.
66
+ */
67
+ export async function verifyTransparencyDeclaration(declaration) {
68
+ // Structural sanity — anything malformed past JSON.parse falls into
69
+ // `malformed_declaration` rather than the more specific reasons. A
70
+ // genuinely tampered declaration produces hash_mismatch or
71
+ // signature_invalid; a malformed-shape declaration is a producer
72
+ // bug or wire-protocol mismatch.
73
+ if (typeof declaration !== "object" ||
74
+ declaration === null ||
75
+ typeof declaration.relay_public_key !== "string" ||
76
+ typeof declaration.signature !== "string" ||
77
+ typeof declaration.hash !== "string" ||
78
+ typeof declaration.suite !== "string" ||
79
+ typeof declaration.relay_id !== "string" ||
80
+ typeof declaration.declared_at !== "number" ||
81
+ typeof declaration.spec !== "string") {
82
+ return { ok: false, reason: "malformed_declaration" };
83
+ }
84
+ // Recompute hash over the signed payload (everything except hash, suite, signature).
85
+ const payload = {
86
+ spec: declaration.spec,
87
+ declared_at: declaration.declared_at,
88
+ relay_id: declaration.relay_id,
89
+ relay_public_key: declaration.relay_public_key,
90
+ content: declaration.content,
91
+ };
92
+ const canonical = new TextEncoder().encode(canonicalJson(payload));
93
+ const computedHashBytes = await sha256(canonical);
94
+ const computedHash = bytesToHex(computedHashBytes);
95
+ if (computedHash !== declaration.hash) {
96
+ return { ok: false, reason: "hash_mismatch" };
97
+ }
98
+ // Decode the declared public key. Ed25519 = 32 bytes = 64 hex chars.
99
+ // Future PQ suites will need their own length validation per suite.
100
+ if (!/^[0-9a-fA-F]{64}$/.test(declaration.relay_public_key)) {
101
+ return { ok: false, reason: "malformed_public_key" };
102
+ }
103
+ let publicKey;
104
+ try {
105
+ publicKey = hexToBytes(declaration.relay_public_key);
106
+ }
107
+ catch {
108
+ return { ok: false, reason: "malformed_public_key" };
109
+ }
110
+ // Decode the signature (hex form for transparency declarations).
111
+ if (!/^[0-9a-fA-F]+$/.test(declaration.signature)) {
112
+ return { ok: false, reason: "malformed_signature" };
113
+ }
114
+ let sigBytes;
115
+ try {
116
+ sigBytes = hexToBytes(declaration.signature);
117
+ }
118
+ catch {
119
+ return { ok: false, reason: "malformed_signature" };
120
+ }
121
+ // Verify under the declared suite.
122
+ let valid;
123
+ try {
124
+ valid = await verifyBySuite(declaration.suite, canonical, sigBytes, publicKey);
125
+ }
126
+ catch {
127
+ return { ok: false, reason: "unsupported_suite" };
128
+ }
129
+ if (!valid) {
130
+ return { ok: false, reason: "signature_invalid" };
131
+ }
132
+ return {
133
+ ok: true,
134
+ anchor: {
135
+ relayPublicKey: publicKey,
136
+ relayPublicKeyHex: declaration.relay_public_key.toLowerCase(),
137
+ relayId: declaration.relay_id,
138
+ declaredAt: declaration.declared_at,
139
+ },
140
+ };
141
+ }
142
+ //# sourceMappingURL=transparency-anchor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transparency-anchor.js","sourceRoot":"","sources":["../src/transparency-anchor.ts"],"names":[],"mappings":"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"}