@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.
- package/LICENSE +206 -0
- package/NOTICE +19 -0
- package/README.md +135 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/inner-receipts.d.ts +67 -0
- package/dist/inner-receipts.d.ts.map +1 -0
- package/dist/inner-receipts.js +161 -0
- package/dist/inner-receipts.js.map +1 -0
- package/dist/onchain-anchor.d.ts +93 -0
- package/dist/onchain-anchor.d.ts.map +1 -0
- package/dist/onchain-anchor.js +141 -0
- package/dist/onchain-anchor.js.map +1 -0
- package/dist/transparency-anchor.d.ts +86 -0
- package/dist/transparency-anchor.d.ts.map +1 -0
- package/dist/transparency-anchor.js +142 -0
- package/dist/transparency-anchor.js.map +1 -0
- package/dist/verified-fetch.d.ts +105 -0
- package/dist/verified-fetch.d.ts.map +1 -0
- package/dist/verified-fetch.js +164 -0
- package/dist/verified-fetch.js.map +1 -0
- package/package.json +54 -0
|
@@ -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":"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"}
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA4D/F;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,OAAe,EACf,UAA0C,EAAE;IAE5C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,wCAAwC,CAAC;IACtE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC;IACnD,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IAEpD,IAAI,WAA0C,CAAC;IAC/C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,cAAc;gBACtB,MAAM,EAAE,QAAQ,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE;aAC/C,CAAC;QACJ,CAAC;QACD,WAAW,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAkC,CAAC;IACpE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,cAAc;YACtB,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACzD,CAAC;IACJ,CAAC;IAED,OAAO,6BAA6B,CAAC,WAAW,CAAC,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,WAA0C;IAE1C,oEAAoE;IACpE,mEAAmE;IACnE,2DAA2D;IAC3D,iEAAiE;IACjE,iCAAiC;IACjC,IACE,OAAO,WAAW,KAAK,QAAQ;QAC/B,WAAW,KAAK,IAAI;QACpB,OAAO,WAAW,CAAC,gBAAgB,KAAK,QAAQ;QAChD,OAAO,WAAW,CAAC,SAAS,KAAK,QAAQ;QACzC,OAAO,WAAW,CAAC,IAAI,KAAK,QAAQ;QACpC,OAAO,WAAW,CAAC,KAAK,KAAK,QAAQ;QACrC,OAAO,WAAW,CAAC,QAAQ,KAAK,QAAQ;QACxC,OAAO,WAAW,CAAC,WAAW,KAAK,QAAQ;QAC3C,OAAO,WAAW,CAAC,IAAI,KAAK,QAAQ,EACpC,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;IACxD,CAAC;IAED,qFAAqF;IACrF,MAAM,OAAO,GAAG;QACd,IAAI,EAAE,WAAW,CAAC,IAAI;QACtB,WAAW,EAAE,WAAW,CAAC,WAAW;QACpC,QAAQ,EAAE,WAAW,CAAC,QAAQ;QAC9B,gBAAgB,EAAE,WAAW,CAAC,gBAAgB;QAC9C,OAAO,EAAE,WAAW,CAAC,OAAO;KAC7B,CAAC;IACF,MAAM,SAAS,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACnE,MAAM,iBAAiB,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IACnD,IAAI,YAAY,KAAK,WAAW,CAAC,IAAI,EAAE,CAAC;QACtC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAChD,CAAC;IAED,qEAAqE;IACrE,oEAAoE;IACpE,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC5D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACvD,CAAC;IACD,IAAI,SAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACvD,CAAC;IAED,iEAAiE;IACjE,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;QAClD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IACtD,CAAC;IACD,IAAI,QAAoB,CAAC;IACzB,IAAI,CAAC;QACH,QAAQ,GAAG,UAAU,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IACtD,CAAC;IAED,mCAAmC;IACnC,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IACjF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IACpD,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IACpD,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI;QACR,MAAM,EAAE;YACN,cAAc,EAAE,SAAS;YACzB,iBAAiB,EAAE,WAAW,CAAC,gBAAgB,CAAC,WAAW,EAAE;YAC7D,OAAO,EAAE,WAAW,CAAC,QAAQ;YAC7B,UAAU,EAAE,WAAW,CAAC,WAAW;SACpC;KACF,CAAC;AACJ,CAAC"}
|