@deeeed/metamask-harness 0.38.1 → 0.40.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/adapters/extension/lib/playwright-cdp.cjs +17 -1
  3. package/adapters/extension/sidepanel-toggle.sh +135 -13
  4. package/dist/adapters/mobile/release-artifact.js +65 -0
  5. package/dist/adapters.js +27 -17
  6. package/dist/command-contract.js +10 -2
  7. package/dist/commands/call.js +7 -4
  8. package/dist/commands/launch/index.js +55 -3
  9. package/dist/commands/parse-args.js +6 -0
  10. package/dist/commands/run-engine.js +75 -39
  11. package/dist/funding-execution-context.js +1386 -0
  12. package/dist/live-adapter-contract.js +3 -1
  13. package/dist/metamask-action-validation.js +170 -3
  14. package/dist/mm-harness-cli.js +8 -2
  15. package/dist/recipe-security.js +5 -12
  16. package/dist/runner.js +102 -6
  17. package/docs/RECIPES.md +74 -1
  18. package/docs/RELEASE-QA-CAPABILITY-MAP.md +6 -5
  19. package/library/actions/extension/assets/open_details.mjs +30 -0
  20. package/library/actions/extension/assets/set_token_visibility.mjs +5 -0
  21. package/library/actions/extension/networks/add_chainlist.mjs +4 -0
  22. package/library/actions/extension/networks/add_custom.mjs +4 -0
  23. package/library/actions/extension/networks/custom_network.mjs +48 -1
  24. package/library/actions/extension/perps/assert_visible_consistency.mjs +366 -0
  25. package/library/actions/extension/perps/close_visible_position.mjs +299 -37
  26. package/library/actions/extension/perps/compare_provider_market.mjs +77 -0
  27. package/library/actions/extension/perps/edit_margin.mjs +285 -44
  28. package/library/actions/extension/perps/mutation-receipt.mjs +781 -0
  29. package/library/actions/extension/perps/perps.mjs +1029 -363
  30. package/library/actions/extension/perps/read_funds_confirmation.mjs +126 -13
  31. package/library/actions/extension/perps/read_snapshot.mjs +124 -3
  32. package/library/actions/extension/perps/read_visible_state.mjs +63 -5
  33. package/library/actions/extension/perps/search_markets.mjs +32 -0
  34. package/library/actions/extension/perps/select_activity_filter.mjs +81 -18
  35. package/library/actions/extension/perps/select_market_filter.mjs +48 -4
  36. package/library/actions/extension/perps/set_market_favorite.mjs +6 -1
  37. package/library/actions/extension/perps/update_position_tpsl.mjs +251 -69
  38. package/library/actions/extension/perps/visible-mutation-identity.mjs +164 -0
  39. package/library/actions/extension/platform/cdp.mjs +88 -9
  40. package/library/actions/extension/settings/set_basic_functionality.mjs +35 -7
  41. package/library/actions/extension/swap_bridge/read_visible_state.mjs +2 -6
  42. package/library/actions/extension/swap_bridge/set_amount.mjs +11 -2
  43. package/library/actions/extension/ui/navigate.mjs +45 -5
  44. package/library/actions/extension/wallet/import.mjs +70 -29
  45. package/library/actions/extension/wallet/lock.mjs +62 -5
  46. package/library/actions/extension/wallet/visible-session.mjs +218 -0
  47. package/library/actions/mobile/assets/import_custom_token.mjs +181 -0
  48. package/library/actions/mobile/assets/open_details.mjs +43 -0
  49. package/library/actions/mobile/assets/read_details.mjs +96 -0
  50. package/library/actions/mobile/assets/set_token_visibility.mjs +302 -0
  51. package/library/actions/mobile/assets/verify_sorting.mjs +104 -0
  52. package/library/actions/mobile/networks/add_custom.mjs +125 -0
  53. package/library/actions/mobile/networks/network-management.mjs +132 -0
  54. package/library/actions/mobile/networks/read_visible_state.mjs +43 -0
  55. package/library/actions/mobile/networks/remove_custom.mjs +100 -0
  56. package/library/actions/mobile/perps/search_markets.mjs +46 -0
  57. package/library/actions/mobile/perps/set_market_favorite.mjs +117 -0
  58. package/library/actions/mobile/platform/native-session-name.mjs +14 -0
  59. package/library/actions/mobile/platform/native-session.mjs +59 -0
  60. package/library/actions/mobile/platform/observe-ui.mjs +9 -1
  61. package/library/actions/mobile/swap_bridge/native-session.mjs +10 -45
  62. package/library/actions/mobile/swap_bridge/select_assets.mjs +15 -1
  63. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -1
  64. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +55 -0
  65. package/library/actions/mobile/swap_bridge/set_slippage.mjs +96 -0
  66. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +141 -0
  67. package/library/actions/mobile/ui/native-navigation.mjs +120 -23
  68. package/library/actions/mobile/ui/navigate.mjs +13 -2
  69. package/library/actions/mobile/wallet/import.mjs +17 -10
  70. package/library/actions/mobile/wallet/lock.mjs +14 -0
  71. package/library/actions/mobile/wallet/native-ui.mjs +88 -15
  72. package/library/actions/shared/perps/visible-state.mjs +33 -11
  73. package/library/actions/shared/swap-bridge/visible-state.mjs +15 -0
  74. package/library/manifests/extension.action-manifest.json +1707 -1241
  75. package/library/manifests/mobile.action-manifest.json +308 -1
  76. package/library/recipes/assets/release-token-details.recipe.json +5 -4
  77. package/library/recipes/assets/release-token-hide-readd.recipe.json +2 -0
  78. package/library/recipes/{assets → extension/assets}/release-custom-network-token-persistence.recipe.json +1 -0
  79. package/library/recipes/{perps → extension/perps}/release-activity-filters.recipe.json +5 -5
  80. package/library/recipes/{perps → extension/perps}/release-funds-flows.recipe.json +3 -3
  81. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +24 -19
  82. package/library/recipes/extension/perps/release-market-filters.recipe.json +97 -0
  83. package/library/recipes/extension/perps/release-order-entry.recipe.json +4 -1
  84. package/library/recipes/extension/perps/release-order-validation.recipe.json +53 -14
  85. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +47 -0
  86. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +76 -0
  87. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +90 -0
  88. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +58 -0
  89. package/library/recipes/perps/release-live-market-order.recipe.json +30 -13
  90. package/library/recipes/perps/release-market-details.recipe.json +1 -1
  91. package/library/recipes/perps/release-market-search.recipe.json +8 -4
  92. package/library/recipes/perps/release-watchlist.recipe.json +31 -19
  93. package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +5 -13
  94. package/library/recipes/swap-bridge/release-quote.recipe.json +1 -0
  95. package/library/recipes/wallet/import.recipe.json +4 -14
  96. package/package.json +1 -1
  97. package/library/actions/extension/assets/finish_send.mjs +0 -128
  98. package/library/actions/extension/swap_bridge/submit_transaction.mjs +0 -363
  99. package/library/actions/extension/ui/activate_and_observe.mjs +0 -96
  100. package/library/recipes/assets/release-custom-gas-send.recipe.json +0 -88
  101. package/library/recipes/assets/release-native-max-cancel.recipe.json +0 -70
  102. package/library/recipes/assets/release-native-max-send.recipe.json +0 -78
  103. package/library/recipes/assets/release-own-account-send.recipe.json +0 -80
  104. package/library/recipes/extension/runner/action-validation.recipe.json +0 -343
@@ -0,0 +1,1386 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, readFile, readdir, realpath } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { digestRecipeDocument } from "@farmslot/protocol";
7
+ const SHA256 = /^[a-f0-9]{64}$/u;
8
+ const INTEGER = /^(0|[1-9][0-9]{0,39})$/u;
9
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
10
+ const NODE_ID = /^(?=.{1,256}$)[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,63})*$/u;
11
+ const ORIGIN = /^chrome-extension:\/\/[a-p]{32}$/u;
12
+ const MAX_DOCUMENT_BYTES = 256 * 1024;
13
+ const FUNDING_BALANCE_READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
14
+ "call",
15
+ "end",
16
+ "metamask.perps.read_visible_state",
17
+ "metamask.wallet.import",
18
+ "ui.navigate",
19
+ "ui.wait_for"
20
+ ]);
21
+ const CLAIM_DIFF_FIELDS = [
22
+ "runId",
23
+ "rootRecipeSha256",
24
+ "executionPlanSha256",
25
+ "fundingRequestSha256",
26
+ "fundingPreflightSha256",
27
+ "fundingReleaseProofSha256",
28
+ "fundingBalanceProofSha256",
29
+ "fundingRequirementsSha256",
30
+ "runtimeArtifactSha256",
31
+ "runtimeTreeSha256",
32
+ "artifactsIdentitySha256",
33
+ "cdpTargetId",
34
+ "operationCounts"
35
+ ];
36
+ const trustedActionContexts = /* @__PURE__ */ new WeakSet();
37
+ const fundingSources = /* @__PURE__ */ new WeakMap();
38
+ const CURRENT_FUNDING_REGISTRY_SHA256 = "7e79ee2a5f7bdde0a5548208cb2eab55881575cc7d826ee5f7acdbf15d49609c";
39
+ const FROZEN_G005_FUNDING_REGISTRY_SHA256 = "c2230bc02fa00d6902915d1cb81bcc08389ea7e399ee9618f26fb0d9fc00b291";
40
+ const LIVE_PERPS_TINY_REGISTRY = {
41
+ "live-perps-tiny": {
42
+ perps: {
43
+ clients: /* @__PURE__ */ new Set(["extension", "mobile"]),
44
+ requirements: /* @__PURE__ */ new Map([
45
+ ["metric:perps-available-margin-usdc", { minBaseUnits: "30000000" }]
46
+ ]),
47
+ budgets: /* @__PURE__ */ new Map([
48
+ ["perps-market-order:usdc-base-units", { maxBaseUnits: "11000000", maxCount: 1 }],
49
+ ["perps-close-position:usdc-base-units", { maxBaseUnits: "11000000", maxCount: 1 }],
50
+ ["perps-limit-order:usdc-base-units", { maxBaseUnits: "10000000", maxCount: 1 }],
51
+ ["perps-cancel-order:usdc-base-units", { maxBaseUnits: "10000000", maxCount: 1 }],
52
+ ["perps-margin-adjust:usdc-base-units", { maxBaseUnits: "1000000", maxCount: 2 }],
53
+ ["perps-tpsl-update:usdc-base-units", { maxBaseUnits: "10000000", maxCount: 1 }]
54
+ ])
55
+ }
56
+ }
57
+ };
58
+ const TRUSTED_FUNDING_REGISTRIES = {
59
+ [CURRENT_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY,
60
+ [FROZEN_G005_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY
61
+ };
62
+ const OPERATION_ACTION = /* @__PURE__ */ new Map([
63
+ ["perps-market-order", "metamask.perps.place_order"],
64
+ ["perps-limit-order", "metamask.perps.place_order"],
65
+ ["perps-close-position", "metamask.perps.close_visible_position"],
66
+ ["perps-cancel-order", "metamask.perps.close_orders"],
67
+ ["perps-margin-adjust", "metamask.perps.edit_margin"],
68
+ ["perps-tpsl-update", "metamask.perps.update_position_tpsl"]
69
+ ]);
70
+ async function loadTrustedMutationBase(options) {
71
+ const { fundingRequest, fundingPreflight, fundingReleaseProof, fundingBalanceProof } = options;
72
+ if (!fundingRequest && !fundingPreflight && !fundingReleaseProof && !fundingBalanceProof) return void 0;
73
+ if (!fundingRequest || !fundingPreflight || !fundingReleaseProof || !fundingBalanceProof) {
74
+ throw new Error(
75
+ "External-mutation funding requires --funding-request, --funding-preflight, --funding-release-proof, and --funding-balance-proof. Next: provide the exact preflight input chain."
76
+ );
77
+ }
78
+ if (![fundingRequest, fundingPreflight, fundingReleaseProof, fundingBalanceProof].every(path.isAbsolute)) {
79
+ throw new Error(
80
+ "External-mutation funding files must use absolute paths. Next: pass absolute --funding-request and --funding-preflight paths."
81
+ );
82
+ }
83
+ if (!path.isAbsolute(options.artifactsDir)) {
84
+ throw new Error("Runner artifactsDir must be absolute before funding context is created.");
85
+ }
86
+ if (!isRecord(options.rootRecipe)) {
87
+ throw new Error("External-mutation funding requires the loaded root recipe document.");
88
+ }
89
+ if (!options.projectRoot && !options.testIo?.currentReleaseRuntime) {
90
+ throw new Error("External-mutation funding requires the runner project root for current release-artifact validation.");
91
+ }
92
+ if (options.testIo?.currentReleaseRuntime && process.env.NODE_ENV !== "test") {
93
+ throw new Error("Current release runtime override is test-only.");
94
+ }
95
+ const documents = await readFundingDocuments(
96
+ fundingRequest,
97
+ fundingPreflight,
98
+ fundingReleaseProof,
99
+ fundingBalanceProof
100
+ );
101
+ const identity = await validateFundingDocuments(documents);
102
+ const currentReleaseRuntime = options.testIo?.currentReleaseRuntime ?? releaseRuntimeValidator(path.resolve(options.projectRoot));
103
+ const currentRelease = await currentReleaseRuntime(false, identity.cdpPort);
104
+ if (currentRelease.artifactSha256 !== identity.subjectDigest || currentRelease.treeSha256 !== identity.releaseTreeSha256) {
105
+ throw new Error("Runner-owned current release artifact does not match the funding release proof.");
106
+ }
107
+ const fundingRequirementsSha256 = sha256(canonical(identity.requirements));
108
+ const rootRecipeSha256 = normalizeRecipeDigest(digestRecipeDocument(options.rootRecipe));
109
+ const runId = sha256(JSON.stringify({
110
+ artifactsDir: options.artifactsDir,
111
+ requestSha256: documents.requestSha256,
112
+ preflightSha256: documents.preflightSha256,
113
+ releaseProofSha256: documents.releaseProofSha256,
114
+ balanceProofSha256: documents.balanceProofSha256,
115
+ rootRecipeSha256
116
+ }));
117
+ const trusted = deepFreeze({
118
+ runId,
119
+ rootRecipeSha256,
120
+ requestSha256: documents.requestSha256,
121
+ preflightSha256: documents.preflightSha256,
122
+ releaseProofSha256: documents.releaseProofSha256,
123
+ balanceProofSha256: documents.balanceProofSha256,
124
+ fundingRequirementsSha256,
125
+ runtimeArtifactSha256: currentRelease.artifactSha256,
126
+ runtimeTreeSha256: currentRelease.treeSha256,
127
+ ...identity
128
+ });
129
+ fundingSources.set(trusted, {
130
+ artifactsDir: options.artifactsDir,
131
+ currentReleaseRuntime
132
+ });
133
+ return trusted;
134
+ }
135
+ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
136
+ const source = fundingSources.get(base);
137
+ if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
138
+ const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
139
+ const nodes = Array.isArray(plan.nodes) ? plan.nodes : [];
140
+ const actual = /* @__PURE__ */ new Map();
141
+ for (const node of nodes) {
142
+ if (!node.capabilities.includes("external-mutation")) continue;
143
+ actual.set(node.action, (actual.get(node.action) ?? 0) + 1);
144
+ }
145
+ const expected = /* @__PURE__ */ new Map();
146
+ const operationsByAction = /* @__PURE__ */ new Map();
147
+ for (const operation of base.operations) {
148
+ const action = OPERATION_ACTION.get(operation.operation);
149
+ if (!action) throw new Error(`Funding operation ${operation.operation} has no external-mutation action binding.`);
150
+ expected.set(action, (expected.get(action) ?? 0) + operation.count);
151
+ const names = operationsByAction.get(action) ?? /* @__PURE__ */ new Set();
152
+ names.add(operation.operation);
153
+ operationsByAction.set(action, names);
154
+ }
155
+ if ([...operationsByAction.values()].some((operations) => operations.size !== 1)) {
156
+ throw new Error("Funding request has ambiguous operation types for one external-mutation action.");
157
+ }
158
+ const actions = /* @__PURE__ */ new Set([...actual.keys(), ...expected.keys()]);
159
+ for (const action of actions) {
160
+ if ((actual.get(action) ?? 0) !== (expected.get(action) ?? 0)) {
161
+ throw new Error(`Resolved plan external-mutation count for ${action} does not match the funding request.`);
162
+ }
163
+ }
164
+ if (actual.size === 0) throw new Error("Funding documents require a resolved external-mutation plan.");
165
+ const artifactsIdentitySha256 = await directoryIdentity(source.artifactsDir);
166
+ const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
167
+ const runId = sha256(canonical({
168
+ requestSha256: base.requestSha256,
169
+ preflightSha256: base.preflightSha256,
170
+ releaseProofSha256: base.releaseProofSha256,
171
+ balanceProofSha256: base.balanceProofSha256,
172
+ executionPlanSha256,
173
+ artifactsIdentitySha256
174
+ }));
175
+ const claim = {
176
+ schemaVersion: 1,
177
+ kind: "funding-execution-claim",
178
+ runId,
179
+ rootRecipeSha256: base.rootRecipeSha256,
180
+ executionPlanSha256,
181
+ fundingRequestSha256: base.requestSha256,
182
+ fundingPreflightSha256: base.preflightSha256,
183
+ fundingReleaseProofSha256: base.releaseProofSha256,
184
+ fundingBalanceProofSha256: base.balanceProofSha256,
185
+ fundingRequirementsSha256: base.fundingRequirementsSha256,
186
+ runtimeArtifactSha256: base.runtimeArtifactSha256,
187
+ runtimeTreeSha256: base.runtimeTreeSha256,
188
+ artifactsIdentitySha256,
189
+ cdpTargetId: base.cdpTargetId,
190
+ operationCounts
191
+ };
192
+ if (testIo.ledgerRoot && process.env.NODE_ENV !== "test") {
193
+ throw new Error("Funding execution ledger override is test-only.");
194
+ }
195
+ const executionClaimsRoot = await validatorLedgerRoot(testIo.ledgerRoot);
196
+ const executionLedgerRoot = path.join(path.dirname(executionClaimsRoot), "funding-execution-starts");
197
+ await ensurePrivateDirectory(path.dirname(executionLedgerRoot), executionLedgerRoot);
198
+ const currentExecutionClaimSha256 = sha256(canonical(claim));
199
+ const recoveryAlias = await publishExecutionClaim(
200
+ base,
201
+ claim,
202
+ source.artifactsDir,
203
+ plan,
204
+ executionClaimsRoot
205
+ );
206
+ const authorized = deepFreeze({
207
+ ...base,
208
+ runId: recoveryAlias?.runId ?? runId,
209
+ executionPlanSha256: recoveryAlias?.executionPlanSha256 ?? executionPlanSha256,
210
+ artifactsIdentitySha256,
211
+ ...recoveryAlias ? { recoveryPlanAlias: true } : {}
212
+ });
213
+ fundingSources.set(authorized, {
214
+ ...source,
215
+ executionLedgerRoot,
216
+ executionClaimSha256: recoveryAlias?.executionClaimSha256 ?? currentExecutionClaimSha256
217
+ });
218
+ return authorized;
219
+ }
220
+ async function trustedMutationForAction(base, recipe, selectedIdentitySha256) {
221
+ if (!selectedIdentitySha256 || !SHA256.test(selectedIdentitySha256)) {
222
+ throw new Error(
223
+ "External mutation requires a successful same-run metamask.wallet.import selected identity proof. Next: run metamask.wallet.import before the mutation node."
224
+ );
225
+ }
226
+ if (selectedIdentitySha256 !== base.selectedIdentitySha256) {
227
+ throw new Error("External mutation wallet identity does not match the independently validated funding evidence.");
228
+ }
229
+ const source = fundingSources.get(base);
230
+ if (!source) throw new Error("External mutation requires the original runner-verified funding context.");
231
+ if (!source.executionLedgerRoot || !source.executionClaimSha256) {
232
+ throw new Error("External mutation requires an authorized validator-owned execution ledger.");
233
+ }
234
+ const currentRelease = await source.currentReleaseRuntime(true, base.cdpPort);
235
+ if (currentRelease.artifactSha256 !== base.runtimeArtifactSha256 || currentRelease.treeSha256 !== base.runtimeTreeSha256) {
236
+ throw new Error("Runner-owned current release artifact changed before external mutation.");
237
+ }
238
+ const trusted = deepFreeze({
239
+ ...base,
240
+ recipeSha256: normalizeRecipeDigest(digestRecipeDocument(recipe)),
241
+ selectedIdentitySha256,
242
+ executionLedgerRoot: source.executionLedgerRoot,
243
+ executionClaimSha256: source.executionClaimSha256
244
+ });
245
+ trustedActionContexts.add(trusted);
246
+ return trusted;
247
+ }
248
+ function serializeTrustedMutationContext(value) {
249
+ if (!isRecord(value) || !trustedActionContexts.has(value)) {
250
+ throw new Error("Live adapter refused an untrusted mutation context.");
251
+ }
252
+ const trusted = value;
253
+ return {
254
+ runId: trusted.runId,
255
+ rootRecipeSha256: trusted.rootRecipeSha256,
256
+ ...trusted.executionPlanSha256 ? { executionPlanSha256: trusted.executionPlanSha256 } : {},
257
+ ...trusted.artifactsIdentitySha256 ? { artifactsIdentitySha256: trusted.artifactsIdentitySha256 } : {},
258
+ ...trusted.recoveryPlanAlias ? { recoveryPlanAlias: true } : {},
259
+ recipeSha256: trusted.recipeSha256,
260
+ executionLedgerRoot: trusted.executionLedgerRoot,
261
+ executionClaimSha256: trusted.executionClaimSha256,
262
+ requestSha256: trusted.requestSha256,
263
+ preflightSha256: trusted.preflightSha256,
264
+ releaseProofSha256: trusted.releaseProofSha256,
265
+ balanceProofSha256: trusted.balanceProofSha256,
266
+ fundingRequirementsSha256: trusted.fundingRequirementsSha256,
267
+ runtimeArtifactSha256: trusted.runtimeArtifactSha256,
268
+ runtimeTreeSha256: trusted.runtimeTreeSha256,
269
+ registrySha256: trusted.registrySha256,
270
+ profileId: trusted.profileId,
271
+ domain: trusted.domain,
272
+ client: trusted.client,
273
+ proofLane: trusted.proofLane,
274
+ targetId: trusted.targetId,
275
+ subjectDigest: trusted.subjectDigest,
276
+ selectedIdentitySha256: trusted.selectedIdentitySha256,
277
+ runtimeSessionId: trusted.runtimeSessionId,
278
+ cdpPort: trusted.cdpPort,
279
+ cdpTargetId: trusted.cdpTargetId,
280
+ extensionOrigin: trusted.extensionOrigin,
281
+ operations: trusted.operations.map((operation) => ({ ...operation })),
282
+ budgets: trusted.budgets.map((budget) => ({ ...budget })),
283
+ requirements: trusted.requirements.map((requirement) => ({ ...requirement }))
284
+ };
285
+ }
286
+ function selectedIdentityFromWalletImport(action, result, expected) {
287
+ if (action !== "metamask.wallet.import" || !isRecord(result)) return void 0;
288
+ const output = isRecord(result.output) ? result.output : void 0;
289
+ const method = output?.method;
290
+ const proofPath = method === "ui-import" ? "extension-visible-onboarding-import" : method === "ui-visible-session" ? "extension-visible-wallet-session" : "";
291
+ if (!output || output.action !== "metamask.wallet.import" || !proofPath || output.credentialSource !== "environment" || output.credentialSourceName !== "MM_RELEASE_SELECTED_WALLET_SRP" || output.proofPath !== proofPath || output.redacted !== true || output.selectedAddressMatched !== true || typeof output.selectedIdentitySha256 !== "string" || !SHA256.test(output.selectedIdentitySha256) || typeof output.runtimeSessionId !== "string" || !IDENTIFIER.test(output.runtimeSessionId) || !Number.isSafeInteger(output.cdpPort) || Number(output.cdpPort) < 1 || Number(output.cdpPort) > 65535 || typeof output.cdpTargetId !== "string" || !IDENTIFIER.test(output.cdpTargetId)) {
292
+ return void 0;
293
+ }
294
+ let origin;
295
+ try {
296
+ const target = new URL(String(output.targetUrl));
297
+ origin = `${target.protocol}//${target.hostname}`;
298
+ } catch {
299
+ return void 0;
300
+ }
301
+ if (!ORIGIN.test(origin) || expected && (output.selectedIdentitySha256 !== expected.selectedIdentitySha256 || output.runtimeSessionId !== expected.runtimeSessionId || output.cdpPort !== expected.cdpPort || output.cdpTargetId !== expected.cdpTargetId || origin !== expected.extensionOrigin)) {
302
+ return void 0;
303
+ }
304
+ return output.selectedIdentitySha256;
305
+ }
306
+ async function readFundingDocuments(requestPath, preflightPath, releaseProofPath, balanceProofPath) {
307
+ const [requestBytes, preflightBytes, releaseProofBytes, balanceProofBytes] = await Promise.all([
308
+ readRegularFile(requestPath, "--funding-request"),
309
+ readRegularFile(preflightPath, "--funding-preflight"),
310
+ readRegularFile(releaseProofPath, "--funding-release-proof"),
311
+ readRegularFile(balanceProofPath, "--funding-balance-proof")
312
+ ]);
313
+ return {
314
+ request: parseObject(requestBytes, "--funding-request"),
315
+ preflight: parseObject(preflightBytes, "--funding-preflight"),
316
+ releaseProof: parseObject(releaseProofBytes, "--funding-release-proof"),
317
+ balanceProof: parseObject(balanceProofBytes, "--funding-balance-proof"),
318
+ requestSha256: sha256(requestBytes),
319
+ preflightSha256: sha256(preflightBytes),
320
+ releaseProofSha256: sha256(releaseProofBytes),
321
+ balanceProofSha256: sha256(balanceProofBytes),
322
+ releaseProofPath: await realpath(releaseProofPath),
323
+ balanceProofPath: await realpath(balanceProofPath)
324
+ };
325
+ }
326
+ async function readRegularFile(file, option) {
327
+ const linkInfo = await lstat(file);
328
+ if (linkInfo.isSymbolicLink() || !linkInfo.isFile()) {
329
+ throw new Error(`${option} must name a regular non-symlink file.`);
330
+ }
331
+ const handle = await open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
332
+ try {
333
+ const info = await handle.stat();
334
+ if (!info.isFile() || info.size <= 0 || info.size > MAX_DOCUMENT_BYTES) {
335
+ throw new Error(`${option} must be a nonempty regular file no larger than ${MAX_DOCUMENT_BYTES} bytes.`);
336
+ }
337
+ return await handle.readFile();
338
+ } finally {
339
+ await handle.close();
340
+ }
341
+ }
342
+ async function directoryIdentity(directory) {
343
+ const resolved = path.resolve(directory);
344
+ const info = await lstat(resolved);
345
+ if (!info.isDirectory() || info.isSymbolicLink() || await realpath(resolved) !== resolved) {
346
+ throw new Error("Runner artifactsDir must be a regular non-symlink directory before funding authorization.");
347
+ }
348
+ return sha256(canonical({ path: resolved, device: String(info.dev), inode: String(info.ino) }));
349
+ }
350
+ async function publishExecutionClaim(base, claim, artifactsDir, plan, directory) {
351
+ const releaseIdentityKey = sha256(canonical({
352
+ registrySha256: base.registrySha256,
353
+ profileId: base.profileId,
354
+ domain: base.domain,
355
+ client: base.client,
356
+ proofLane: base.proofLane,
357
+ targetId: base.targetId,
358
+ subjectDigest: base.subjectDigest
359
+ }));
360
+ const identityKey = sha256(canonical({
361
+ releaseIdentityKey,
362
+ requestSha256: base.requestSha256,
363
+ preflightSha256: base.preflightSha256
364
+ }));
365
+ const stateRoot = path.dirname(directory);
366
+ const claimPath = path.join(directory, `${identityKey}.json`);
367
+ const bytes = canonical(claim);
368
+ let currentClaimExists = true;
369
+ try {
370
+ await lstat(claimPath);
371
+ } catch (error) {
372
+ if (error.code !== "ENOENT") throw error;
373
+ currentClaimExists = false;
374
+ }
375
+ if (!currentClaimExists) {
376
+ const legacyRecovery = await recoverLegacyExecutionClaim(
377
+ path.join(directory, `${releaseIdentityKey}.json`),
378
+ bytes,
379
+ artifactsDir,
380
+ base,
381
+ plan
382
+ );
383
+ if (legacyRecovery) return legacyRecovery;
384
+ }
385
+ try {
386
+ const handle = await open(claimPath, "wx", 384);
387
+ try {
388
+ await handle.writeFile(bytes);
389
+ await handle.sync();
390
+ } finally {
391
+ await handle.close();
392
+ }
393
+ await syncDirectory(directory);
394
+ await syncDirectory(stateRoot);
395
+ } catch (error) {
396
+ if (error.code !== "EEXIST") throw error;
397
+ const info = await lstat(claimPath);
398
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("Funding execution claim is unsafe.");
399
+ const existing = await readFile(claimPath, "utf8");
400
+ if (existing !== bytes) {
401
+ const comparison = compareExecutionClaims(existing, bytes);
402
+ if (comparison.differences.length === 2 && comparison.differences.includes("runId") && comparison.differences.includes("executionPlanSha256")) {
403
+ await requireSafeRecoveryReceiptChain(artifactsDir, comparison.existing, base, plan);
404
+ await syncDirectory(directory);
405
+ await syncDirectory(stateRoot);
406
+ return {
407
+ runId: comparison.existing.runId,
408
+ executionPlanSha256: comparison.existing.executionPlanSha256,
409
+ executionClaimSha256: sha256(existing)
410
+ };
411
+ }
412
+ throw new Error(executionClaimMismatch(comparison));
413
+ }
414
+ await syncDirectory(directory);
415
+ await syncDirectory(stateRoot);
416
+ }
417
+ return void 0;
418
+ }
419
+ async function recoverLegacyExecutionClaim(claimPath, currentBytes, artifactsDir, base, plan) {
420
+ let info;
421
+ try {
422
+ info = await lstat(claimPath);
423
+ } catch (error) {
424
+ if (error.code === "ENOENT") return void 0;
425
+ throw error;
426
+ }
427
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("Funding execution claim is unsafe.");
428
+ const existingBytes = await readFile(claimPath, "utf8");
429
+ const comparison = compareExecutionClaims(existingBytes, currentBytes);
430
+ const sameFundingDocuments = comparison.existing.fundingRequestSha256 === comparison.current.fundingRequestSha256 && comparison.existing.fundingPreflightSha256 === comparison.current.fundingPreflightSha256 && comparison.existing.fundingReleaseProofSha256 === comparison.current.fundingReleaseProofSha256 && comparison.existing.fundingBalanceProofSha256 === comparison.current.fundingBalanceProofSha256;
431
+ if (!sameFundingDocuments) return void 0;
432
+ if (existingBytes !== currentBytes) throw new Error(executionClaimMismatch(comparison));
433
+ await requireSafeRecoveryReceiptChain(artifactsDir, comparison.existing, base, plan);
434
+ return {
435
+ runId: comparison.existing.runId,
436
+ executionPlanSha256: comparison.existing.executionPlanSha256,
437
+ executionClaimSha256: sha256(existingBytes)
438
+ };
439
+ }
440
+ function compareExecutionClaims(existingBytes, currentBytes) {
441
+ let existing;
442
+ let current;
443
+ try {
444
+ existing = parseExecutionClaimIdentity(existingBytes);
445
+ current = parseExecutionClaimIdentity(currentBytes);
446
+ } catch {
447
+ throw new Error("Funding execution claim is unsafe.");
448
+ }
449
+ const differences = CLAIM_DIFF_FIELDS.filter((field) => JSON.stringify(existing[field]) !== JSON.stringify(current[field]));
450
+ return { existing, current, differences };
451
+ }
452
+ function executionClaimMismatch(comparison) {
453
+ const differences = comparison.differences.map((field) => `${field} existing=${JSON.stringify(comparison.existing[field])} current=${JSON.stringify(comparison.current[field])}`);
454
+ const detail = differences.length > 0 ? ` Claim differences: ${differences.join("; ")}.` : "";
455
+ return `Funding request is already claimed by a different plan, artifacts directory, or execution.${detail}`;
456
+ }
457
+ function parseExecutionClaimIdentity(bytes) {
458
+ const value = JSON.parse(bytes);
459
+ if (!isRecord(value)) throw new Error("claim must be an object");
460
+ const expectedKeys = ["schemaVersion", "kind", ...CLAIM_DIFF_FIELDS].sort();
461
+ if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(expectedKeys) || value.schemaVersion !== 1 || value.kind !== "funding-execution-claim") {
462
+ throw new Error("claim shape is invalid");
463
+ }
464
+ const identity = {};
465
+ for (const field of CLAIM_DIFF_FIELDS.slice(0, -2)) {
466
+ if (typeof value[field] !== "string" || !SHA256.test(value[field])) {
467
+ throw new Error(`claim ${field} is invalid`);
468
+ }
469
+ identity[field] = value[field];
470
+ }
471
+ if (typeof value.cdpTargetId !== "string" || !IDENTIFIER.test(value.cdpTargetId)) {
472
+ throw new Error("claim cdpTargetId is invalid");
473
+ }
474
+ identity.cdpTargetId = value.cdpTargetId;
475
+ if (!Array.isArray(value.operationCounts) || value.operationCounts.length < 1 || value.operationCounts.length > 32) {
476
+ throw new Error("claim operationCounts is invalid");
477
+ }
478
+ identity.operationCounts = value.operationCounts.map((entry) => {
479
+ if (!isRecord(entry) || typeof entry.action !== "string" || !IDENTIFIER.test(entry.action) || !Number.isInteger(entry.count) || Number(entry.count) < 1 || Number(entry.count) > 1e3) {
480
+ throw new Error("claim operationCounts entry is invalid");
481
+ }
482
+ return { action: entry.action, count: Number(entry.count) };
483
+ });
484
+ return identity;
485
+ }
486
+ async function requireSafeRecoveryReceiptChain(artifactsDir, claim, base, plan) {
487
+ const receiptDirectory = path.join(artifactsDir, "mutation-receipts");
488
+ let directoryInfo;
489
+ try {
490
+ directoryInfo = await lstat(receiptDirectory);
491
+ } catch {
492
+ throw new Error("Funding recovery requires a safe external mutation receipt chain.");
493
+ }
494
+ if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink() || await realpath(receiptDirectory) !== receiptDirectory) {
495
+ throw new Error("Funding recovery receipt directory is unsafe.");
496
+ }
497
+ const externalNodes = plan.nodes.filter((node) => node.capabilities.includes("external-mutation"));
498
+ if (externalNodes.length < 1 || new Set(externalNodes.map((node) => node.nodeId)).size !== externalNodes.length) {
499
+ throw new Error("Funding recovery external mutation plan is unsafe.");
500
+ }
501
+ const entries = await readdir(receiptDirectory, { withFileTypes: true });
502
+ if (entries.length < 1 || entries.length > externalNodes.length || entries.some((entry) => !entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9]{64}\.json$/u.test(entry.name))) {
503
+ throw new Error("Funding recovery receipt chain contains unsafe or extra entries.");
504
+ }
505
+ const matches = [
506
+ ["runId", "runId"],
507
+ ["rootRecipeSha256", "rootRecipeSha256"],
508
+ ["executionPlanSha256", "executionPlanSha256"],
509
+ ["artifactsIdentitySha256", "artifactsIdentitySha256"],
510
+ ["fundingRequestSha256", "fundingRequestSha256"],
511
+ ["fundingPreflightSha256", "fundingPreflightSha256"],
512
+ ["fundingReleaseProofSha256", "fundingReleaseProofSha256"],
513
+ ["fundingBalanceProofSha256", "fundingBalanceProofSha256"],
514
+ ["fundingRequirementsSha256", "fundingRequirementsSha256"],
515
+ ["runtimeArtifactSha256", "runtimeArtifactSha256"],
516
+ ["runtimeTreeSha256", "runtimeTreeSha256"]
517
+ ];
518
+ const receiptByNode = /* @__PURE__ */ new Map();
519
+ let runtimeIdentity;
520
+ for (const entry of entries) {
521
+ const receiptPath = path.join(receiptDirectory, entry.name);
522
+ const receiptInfo = await lstat(receiptPath);
523
+ if (!receiptInfo.isFile() || receiptInfo.isSymbolicLink() || receiptInfo.size <= 0 || receiptInfo.size > MAX_DOCUMENT_BYTES) {
524
+ throw new Error("Funding recovery mutation receipt is unsafe.");
525
+ }
526
+ const handle = await open(receiptPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
527
+ let bytes;
528
+ try {
529
+ const openedInfo = await handle.stat();
530
+ if (!openedInfo.isFile() || openedInfo.size !== receiptInfo.size) {
531
+ throw new Error("Funding recovery mutation receipt changed during inspection.");
532
+ }
533
+ bytes = await handle.readFile();
534
+ } finally {
535
+ await handle.close();
536
+ }
537
+ let receipt;
538
+ try {
539
+ receipt = parseObject(bytes, "Funding recovery mutation receipt");
540
+ } catch {
541
+ throw new Error("Funding recovery mutation receipt is unsafe.");
542
+ }
543
+ if (canonical(receipt) !== bytes.toString("utf8") || receipt.schemaVersion !== 1 || receipt.kind !== "external-mutation-receipt" || !isRecord(receipt.identity) || receipt.state !== "succeeded" && receipt.state !== "outcome-unknown") {
544
+ throw new Error("Funding recovery mutation receipt shape or state is unsafe.");
545
+ }
546
+ const identity = receipt.identity;
547
+ for (const [claimField, receiptField] of matches) {
548
+ if (typeof identity[receiptField] !== "string" || !SHA256.test(identity[receiptField]) || identity[receiptField] !== claim[claimField]) {
549
+ throw new Error(`Funding recovery receipt identity ${receiptField} does not match the existing claim.`);
550
+ }
551
+ }
552
+ const nodeId = String(identity.nodeId ?? "");
553
+ if (!NODE_ID.test(nodeId) || entry.name !== `${sha256(nodeId)}.json` || identity.executionId !== sha256(`${claim.runId}\0${nodeId}`)) {
554
+ throw new Error("Funding recovery receipt node digest is invalid.");
555
+ }
556
+ const node = externalNodes.find((candidate) => candidate.nodeId === nodeId);
557
+ const operation = base.operations.filter((candidate) => OPERATION_ACTION.get(candidate.operation) === identity.action && candidate.operation === identity.operation && candidate.amountBaseUnits === identity.amountBaseUnits);
558
+ if (!node || node.action !== identity.action || operation.length !== 1) {
559
+ throw new Error("Funding recovery receipt does not map uniquely to an authorized external node.");
560
+ }
561
+ const currentRuntimeIdentity = canonical({
562
+ selectedIdentitySha256: identity.selectedIdentitySha256,
563
+ cdpPort: identity.cdpPort,
564
+ runtimeSessionId: identity.runtimeSessionId,
565
+ cdpTargetId: identity.cdpTargetId,
566
+ extensionOrigin: identity.extensionOrigin
567
+ });
568
+ if (!SHA256.test(String(identity.selectedIdentitySha256 ?? "")) || !Number.isInteger(identity.cdpPort) || Number(identity.cdpPort) < 1 || Number(identity.cdpPort) > 65535 || !IDENTIFIER.test(String(identity.runtimeSessionId ?? "")) || !IDENTIFIER.test(String(identity.cdpTargetId ?? "")) || !ORIGIN.test(String(identity.extensionOrigin ?? "")) || identity.selectedIdentitySha256 !== base.selectedIdentitySha256 || identity.cdpPort !== base.cdpPort || identity.runtimeSessionId !== base.runtimeSessionId || identity.cdpTargetId !== base.cdpTargetId || identity.cdpTargetId !== claim.cdpTargetId || identity.extensionOrigin !== base.extensionOrigin || runtimeIdentity !== void 0 && runtimeIdentity !== currentRuntimeIdentity) {
569
+ throw new Error("Funding recovery receipt wallet or runtime identity drifted.");
570
+ }
571
+ runtimeIdentity = currentRuntimeIdentity;
572
+ if (receipt.state === "succeeded") {
573
+ if (!isRecord(receipt.observation) || typeof receipt.observationSha256 !== "string" || receipt.observationSha256 !== sha256(canonical(receipt.observation))) {
574
+ throw new Error("Funding recovery succeeded receipt observation digest is invalid.");
575
+ }
576
+ } else if ("observation" in receipt || "observationSha256" in receipt) {
577
+ throw new Error("Funding recovery outcome-unknown receipt contains a completed observation.");
578
+ }
579
+ if (receiptByNode.has(nodeId)) throw new Error("Funding recovery receipt chain has a duplicate node.");
580
+ receiptByNode.set(nodeId, receipt);
581
+ }
582
+ let sawOutcomeUnknown = false;
583
+ for (let index = 0; index < externalNodes.length; index += 1) {
584
+ const receipt = receiptByNode.get(externalNodes[index].nodeId);
585
+ if (!receipt) {
586
+ if (index !== receiptByNode.size) throw new Error("Funding recovery receipt chain has a gap or later receipt.");
587
+ break;
588
+ }
589
+ if (receipt.state === "outcome-unknown") {
590
+ if (sawOutcomeUnknown || index !== receiptByNode.size - 1) {
591
+ throw new Error("Funding recovery allows at most one outcome-unknown receipt at the next boundary.");
592
+ }
593
+ sawOutcomeUnknown = true;
594
+ } else if (sawOutcomeUnknown) {
595
+ throw new Error("Funding recovery succeeded receipt follows an outcome-unknown boundary.");
596
+ }
597
+ }
598
+ }
599
+ async function validatorLedgerRoot(testLedgerRoot) {
600
+ if (testLedgerRoot) {
601
+ const directory = path.resolve(testLedgerRoot);
602
+ await ensurePrivateDirectory(path.dirname(directory), directory);
603
+ return directory;
604
+ }
605
+ const home = await realpath(os.homedir());
606
+ const harnessRoot = path.join(home, ".mm-harness");
607
+ const stateRoot = path.join(harnessRoot, "state");
608
+ const ledgerRoot = path.join(stateRoot, "funding-execution-claims");
609
+ await ensurePrivateDirectory(home, harnessRoot);
610
+ await ensurePrivateDirectory(harnessRoot, stateRoot);
611
+ await ensurePrivateDirectory(stateRoot, ledgerRoot);
612
+ return ledgerRoot;
613
+ }
614
+ async function ensurePrivateDirectory(parent, directory) {
615
+ const resolvedParent = path.resolve(parent);
616
+ if (await realpath(resolvedParent) !== resolvedParent || path.dirname(path.resolve(directory)) !== resolvedParent) {
617
+ throw new Error("Funding execution ledger must remain inside its validator-owned state root.");
618
+ }
619
+ let created = false;
620
+ try {
621
+ await mkdir(directory, { mode: 448 });
622
+ created = true;
623
+ } catch (error) {
624
+ if (error.code !== "EEXIST") throw error;
625
+ }
626
+ const info = await lstat(directory);
627
+ if (!info.isDirectory() || info.isSymbolicLink() || await realpath(directory) !== path.resolve(directory)) {
628
+ throw new Error("Funding execution ledger directory is unsafe.");
629
+ }
630
+ await chmod(directory, 448);
631
+ if (created) await syncDirectory(resolvedParent);
632
+ }
633
+ async function syncDirectory(directory) {
634
+ const handle = await open(directory, "r");
635
+ try {
636
+ await handle.sync();
637
+ } finally {
638
+ await handle.close();
639
+ }
640
+ }
641
+ function parseObject(bytes, option) {
642
+ try {
643
+ const value = JSON.parse(bytes.toString("utf8"));
644
+ if (!isRecord(value)) throw new Error("root must be an object");
645
+ return value;
646
+ } catch (error) {
647
+ throw new Error(`${option} is not a valid JSON object: ${error instanceof Error ? error.message : String(error)}`);
648
+ }
649
+ }
650
+ async function validateFundingDocuments(documents) {
651
+ const request = documents.request;
652
+ const preflight = documents.preflight;
653
+ if (preflight.status !== "pass" || preflight.fundingReady !== true || preflight.mutationEligible !== true) {
654
+ throw new Error("Funding preflight is not eligible for external mutation.");
655
+ }
656
+ const mutation = requireRecord(preflight.mutation, "funding preflight mutation");
657
+ if (mutation.requested !== true || mutation.consent !== true) {
658
+ throw new Error("Funding preflight must bind a requested mutation with explicit consent.");
659
+ }
660
+ if (request.consent !== true) throw new Error("Funding request consent must be true.");
661
+ if (mutation.requestSha256 !== documents.requestSha256) {
662
+ throw new Error("Funding request bytes do not match funding preflight mutation.requestSha256.");
663
+ }
664
+ const requestOperations = parseOperations(request.operations, "funding request operations");
665
+ const preflightOperations = parseOperations(mutation.operations, "funding preflight operations");
666
+ if (JSON.stringify(requestOperations) !== JSON.stringify(preflightOperations)) {
667
+ throw new Error("Funding preflight operations do not exactly match the funding request.");
668
+ }
669
+ const budgets = parseBudgets(mutation.budgets);
670
+ for (const operation of requestOperations) {
671
+ const budget = budgets.find((candidate) => candidate.operation === operation.operation && candidate.unit === operation.unit);
672
+ if (!budget || BigInt(operation.amountBaseUnits) > BigInt(budget.maxBaseUnits) || operation.count > budget.maxCount) {
673
+ throw new Error(`Funding operation ${operation.operation} exceeds or lacks its preflight budget.`);
674
+ }
675
+ }
676
+ const identity = requireRecord(preflight.identity, "funding preflight identity");
677
+ const fields = ["registrySha256", "profileId", "domain", "client", "proofLane", "targetId", "subjectDigest"];
678
+ const values = {};
679
+ for (const field of fields) {
680
+ const requestValue = requireIdentifier(request[field], `funding request ${field}`, field.endsWith("Sha256") || field === "subjectDigest");
681
+ const preflightValue = requireIdentifier(identity[field], `funding preflight identity ${field}`, field.endsWith("Sha256") || field === "subjectDigest");
682
+ if (requestValue !== preflightValue) throw new Error(`Funding identity ${field} does not match the request.`);
683
+ values[field] = requestValue;
684
+ }
685
+ validateTrustedFundingRegistry(values, requestOperations, budgets);
686
+ const evidence = await validateFundingEvidenceChain(documents, values);
687
+ const preflightRequirements = parsePreflightRequirements(preflight.requirements);
688
+ if (JSON.stringify(preflightRequirements) !== JSON.stringify(evidence.requirements)) {
689
+ throw new Error("Funding preflight requirements do not match the independently validated balance evidence.");
690
+ }
691
+ return {
692
+ ...values,
693
+ ...evidence.runtime,
694
+ operations: requestOperations,
695
+ budgets,
696
+ requirements: evidence.requirements,
697
+ releaseTreeSha256: evidence.releaseTreeSha256
698
+ };
699
+ }
700
+ async function validateFundingEvidenceChain(documents, identity) {
701
+ const releaseProof = documents.releaseProof;
702
+ const releaseRoot = path.dirname(documents.releaseProofPath);
703
+ if (!documents.balanceProofPath.startsWith(`${releaseRoot}${path.sep}`)) {
704
+ throw new Error("Funding balance proof must be contained by the release-proof root.");
705
+ }
706
+ for (const field of ["client", "proofLane", "targetId", "subjectDigest"]) {
707
+ if (releaseProof[field] !== identity[field]) {
708
+ throw new Error(`Funding release proof ${field} does not match the request identity.`);
709
+ }
710
+ }
711
+ const policy = requireRecord(releaseProof.fundingPolicy, "funding release proof fundingPolicy");
712
+ if (policy.required !== true || policy.registrySha256 !== identity.registrySha256 || policy.profileId !== identity.profileId || policy.domain !== identity.domain) {
713
+ throw new Error("Funding release proof does not bind the runner-trusted funding policy.");
714
+ }
715
+ const artifact = requireRecord(releaseProof.artifact, "funding release proof artifact");
716
+ if (artifact.sha256 !== identity.subjectDigest || !SHA256.test(String(artifact.treeSha256 ?? ""))) {
717
+ throw new Error("Funding release proof artifact identity is invalid.");
718
+ }
719
+ const runtime = requireRecord(releaseProof.runtime, "funding release proof runtime");
720
+ const extensionRuntime = requireRecord(runtime.extension, "funding release proof Extension runtime");
721
+ if (identity.client !== "extension" || identity.proofLane !== "official-unmodified" || runtime.fixtures !== false || runtime.instrumentation !== false || extensionRuntime.cleanProfile !== true || extensionRuntime.runtimeTreeSha256 !== artifact.treeSha256) {
722
+ throw new Error("Funding release proof is not an unmodified clean Extension runtime.");
723
+ }
724
+ const onboarding = requireRecord(releaseProof.walletOnboarding, "funding release proof walletOnboarding");
725
+ const onboardingRuntime = requireRecord(onboarding.runtime, "funding release proof wallet runtime");
726
+ const onboardingTracePath = await containedRegularPath(releaseRoot, onboarding.evidence, "funding release onboarding evidence");
727
+ if (sha256(await readFile(onboardingTracePath)) !== onboarding.evidenceSha256) {
728
+ throw new Error("Funding release onboarding evidence digest is invalid.");
729
+ }
730
+ const onboardingRun = requireRecord(onboarding.run, "funding release onboarding run");
731
+ const onboardingArtifact = async (field) => {
732
+ const reference = requireRecord(onboardingRun[field], `funding release onboarding run ${field}`);
733
+ const artifactPath = await containedRegularPath(
734
+ releaseRoot,
735
+ reference.path,
736
+ `funding release onboarding run ${field}`
737
+ );
738
+ const bytes = await readFile(artifactPath);
739
+ if (reference.sha256 !== sha256(bytes)) {
740
+ throw new Error(`Funding release onboarding run ${field} digest is invalid.`);
741
+ }
742
+ return { document: parseObject(bytes, `funding release onboarding run ${field}`), artifactPath };
743
+ };
744
+ const onboardingRecipeArtifact = await onboardingArtifact("recipe");
745
+ const onboardingResolutionArtifact = await onboardingArtifact("recipeResolution");
746
+ const onboardingTraceArtifact = await onboardingArtifact("trace");
747
+ const onboardingSummaryArtifact = await onboardingArtifact("summary");
748
+ const onboardingManifestArtifact = await onboardingArtifact("artifactManifest");
749
+ const onboardingPaths = [
750
+ onboardingRecipeArtifact,
751
+ onboardingResolutionArtifact,
752
+ onboardingTraceArtifact,
753
+ onboardingSummaryArtifact,
754
+ onboardingManifestArtifact
755
+ ].map(({ artifactPath }) => artifactPath);
756
+ if (new Set(onboardingPaths).size !== onboardingPaths.length || onboardingTraceArtifact.artifactPath !== onboardingTracePath || onboardingRun.trace && requireRecord(onboardingRun.trace, "funding release onboarding run trace").sha256 !== onboarding.evidenceSha256) {
757
+ throw new Error("Funding release onboarding typed run does not bind its legacy trace evidence.");
758
+ }
759
+ const onboardingDirectory = path.dirname(onboardingRecipeArtifact.artifactPath);
760
+ for (const [artifactPath, name] of [
761
+ [onboardingRecipeArtifact.artifactPath, "recipe.json"],
762
+ [onboardingResolutionArtifact.artifactPath, "recipe-resolution.json"],
763
+ [onboardingTraceArtifact.artifactPath, "trace.json"],
764
+ [onboardingSummaryArtifact.artifactPath, "summary.json"],
765
+ [onboardingManifestArtifact.artifactPath, "artifact-manifest.json"]
766
+ ]) {
767
+ if (artifactPath !== path.join(onboardingDirectory, name)) {
768
+ throw new Error(`Funding release onboarding run does not bind emitted ${name}.`);
769
+ }
770
+ }
771
+ const onboardingRecipe = onboardingRecipeArtifact.document;
772
+ const onboardingResolution = onboardingResolutionArtifact.document;
773
+ const onboardingTrace = onboardingTraceArtifact.document;
774
+ const onboardingSummary = onboardingSummaryArtifact.document;
775
+ const onboardingManifest = onboardingManifestArtifact.document;
776
+ const onboardingTraceMetadata = requireRecord(onboardingTrace.metadata, "funding release onboarding trace metadata");
777
+ const onboardingRunner = requireRecord(onboardingTraceMetadata.runner, "funding release onboarding trace runner");
778
+ const onboardingManifestProvenance = requireRecord(onboardingManifest.provenance, "funding release onboarding artifact provenance");
779
+ if (JSON.stringify(onboardingRunner) !== JSON.stringify(onboardingSummary.runner) || JSON.stringify(onboardingRunner) !== JSON.stringify(onboardingManifestProvenance.runner)) {
780
+ throw new Error("Funding release onboarding runner provenance differs across typed artifacts.");
781
+ }
782
+ const onboardingResolutionRoot = requireRecord(onboardingResolution.root, "funding release onboarding resolution root");
783
+ if (onboardingResolutionRoot.digest !== `sha256:${normalizeRecipeDigest(digestRecipeDocument(onboardingRecipe))}` || typeof onboardingResolutionRoot.ref !== "string" || onboardingResolutionRoot.ref.length === 0 || JSON.stringify(Object.keys(onboardingResolutionRoot).sort()) !== JSON.stringify(["digest", "ref"]) || !Array.isArray(onboardingResolution.dependencies) || !Array.isArray(onboardingResolution.edges)) {
784
+ throw new Error("Funding release onboarding resolution does not bind its emitted recipe graph.");
785
+ }
786
+ const onboardingDependencies = /* @__PURE__ */ new Map();
787
+ for (const [index, value] of onboardingResolution.dependencies.entries()) {
788
+ const dependency = requireRecord(value, `funding release onboarding dependency[${index}]`);
789
+ if (dependency.ref === onboardingResolutionRoot.ref) {
790
+ throw new Error("Funding release onboarding dependency ref shadows the root ref.");
791
+ }
792
+ const ref = requireIdentifier(dependency.ref, `funding release onboarding dependency[${index}].ref`);
793
+ const digest = String(dependency.digest ?? "");
794
+ if (!digest.startsWith("sha256:") || !SHA256.test(digest.slice(7)) || onboardingDependencies.has(ref)) {
795
+ throw new Error("Funding release onboarding dependency identity is invalid.");
796
+ }
797
+ const artifactName = `resolved-recipes/${digest.slice(7)}.recipe.json`;
798
+ if (dependency.artifact !== artifactName) throw new Error("Funding release onboarding dependency artifact is invalid.");
799
+ const dependencyPath = await containedRegularPath(
800
+ releaseRoot,
801
+ path.relative(releaseRoot, path.resolve(onboardingDirectory, artifactName)),
802
+ `funding release onboarding dependency ${ref}`
803
+ );
804
+ const dependencyDocument = parseObject(await readFile(dependencyPath), `funding release onboarding dependency ${ref}`);
805
+ if (`sha256:${normalizeRecipeDigest(digestRecipeDocument(dependencyDocument))}` !== digest) {
806
+ throw new Error(`Funding release onboarding dependency digest is invalid: ${ref}.`);
807
+ }
808
+ onboardingDependencies.set(ref, dependencyDocument);
809
+ }
810
+ const onboardingArtifacts = Array.isArray(onboardingManifest.artifacts) ? onboardingManifest.artifacts : [];
811
+ for (const required of [
812
+ "recipe.json",
813
+ "recipe-resolution.json",
814
+ "trace.json",
815
+ "summary.json",
816
+ ...onboardingResolution.dependencies.map((value) => isRecord(value) ? value.artifact : void 0)
817
+ ]) {
818
+ if (typeof required !== "string" || !onboardingArtifacts.some((value) => isRecord(value) && value.path === required)) {
819
+ throw new Error(`Funding release onboarding artifact manifest does not bind ${String(required)}.`);
820
+ }
821
+ }
822
+ try {
823
+ validateResolvedTrace(onboardingRecipe, onboardingResolution, onboardingDependencies, onboardingTrace);
824
+ } catch (error) {
825
+ throw new Error(`Funding release onboarding trace is invalid: ${error instanceof Error ? error.message : String(error)}`);
826
+ }
827
+ if (!exactTerminalPass(onboardingRecipe, onboardingTrace, onboardingSummary, onboardingManifest)) {
828
+ throw new Error("Funding release onboarding trace is not exact terminal-pass.");
829
+ }
830
+ const onboardingWallet = successfulWalletOutput(
831
+ onboardingTrace,
832
+ "funding release onboarding evidence",
833
+ "ui-import",
834
+ "extension-visible-onboarding-import"
835
+ );
836
+ const onboardingOrigin = extensionOrigin(onboardingWallet.targetUrl, "funding release onboarding wallet");
837
+ if (onboardingWallet.runtimeSessionId !== onboardingRuntime.runtimeSessionId || onboardingWallet.cdpPort !== onboardingRuntime.cdpPort || onboardingWallet.cdpTargetId !== onboardingRuntime.cdpTargetId || extensionOrigin(onboardingRuntime.targetUrl, "funding release onboarding runtime") !== onboardingOrigin) {
838
+ throw new Error("Funding release onboarding runtime does not match its visible wallet trace.");
839
+ }
840
+ const balanceProof = documents.balanceProof;
841
+ for (const field of ["registrySha256", "profileId", "domain", "client", "proofLane", "targetId", "subjectDigest"]) {
842
+ if (balanceProof[field] !== identity[field]) {
843
+ throw new Error(`Funding balance proof ${field} does not match the request identity.`);
844
+ }
845
+ }
846
+ if (balanceProof.credentialMode !== "environment") {
847
+ throw new Error("Funding balance proof credential mode is invalid.");
848
+ }
849
+ const trustedRequirements = trustedFundingRequirements(identity);
850
+ if (!Array.isArray(balanceProof.observations) || balanceProof.observations.length !== trustedRequirements.size) {
851
+ throw new Error("Funding balance proof does not contain the exact trusted requirements.");
852
+ }
853
+ const seen = /* @__PURE__ */ new Set();
854
+ const requirements = [];
855
+ let balanceRuntime;
856
+ for (const [index, value] of balanceProof.observations.entries()) {
857
+ const observation = requireRecord(value, `funding balance proof observations[${index}]`);
858
+ const kind = requireIdentifier(observation.kind, `funding balance proof observations[${index}].kind`);
859
+ const id = requireIdentifier(observation.id, `funding balance proof observations[${index}].id`);
860
+ const key = `${kind}:${id}`;
861
+ if (seen.has(key)) throw new Error("Funding balance proof contains a duplicate requirement.");
862
+ seen.add(key);
863
+ const trusted = trustedRequirements.get(key);
864
+ const actualBaseUnits = requireInteger(
865
+ observation.actualBaseUnits,
866
+ `funding balance proof observations[${index}].actualBaseUnits`
867
+ );
868
+ if (!trusted || BigInt(actualBaseUnits) < BigInt(trusted.minBaseUnits)) {
869
+ throw new Error(`Funding balance proof requirement ${key} is missing or below its trusted minimum.`);
870
+ }
871
+ const evidence = requireRecord(observation.evidence, `funding balance proof observations[${index}].evidence`);
872
+ for (const field of ["registrySha256", "profileId", "domain", "client", "proofLane", "targetId", "subjectDigest"]) {
873
+ if (evidence[field] !== identity[field]) throw new Error(`Funding balance evidence ${field} does not match.`);
874
+ }
875
+ const evidencePath = await containedRegularPath(
876
+ releaseRoot,
877
+ path.relative(releaseRoot, path.resolve(path.dirname(documents.balanceProofPath), String(evidence.path ?? ""))),
878
+ `funding balance proof observations[${index}].evidence`
879
+ );
880
+ const evidenceBytes = await readFile(evidencePath);
881
+ const evidenceSha256 = sha256(evidenceBytes);
882
+ if (evidence.sha256 !== evidenceSha256) throw new Error("Funding balance evidence digest is invalid.");
883
+ const evidenceDocument = parseObject(evidenceBytes, "funding balance evidence");
884
+ const containedEvidence = async (field) => {
885
+ const record = requireRecord(evidenceDocument[field], `funding balance evidence ${field}`);
886
+ const artifactPath = await containedRegularPath(
887
+ releaseRoot,
888
+ path.relative(releaseRoot, path.resolve(path.dirname(evidencePath), String(record.path ?? ""))),
889
+ `funding balance evidence ${field}`
890
+ );
891
+ const bytes = await readFile(artifactPath);
892
+ if (record.sha256 !== sha256(bytes)) throw new Error(`Funding balance evidence ${field} digest is invalid.`);
893
+ return {
894
+ document: parseObject(bytes, `funding balance evidence ${field}`),
895
+ artifactPath,
896
+ path: String(record.path),
897
+ sha256: String(record.sha256)
898
+ };
899
+ };
900
+ const recipeArtifact = await containedEvidence("recipe");
901
+ const resolutionArtifact = await containedEvidence("recipeResolution");
902
+ const traceArtifact = await containedEvidence("trace");
903
+ const summaryArtifact = await containedEvidence("summary");
904
+ const manifestArtifact = await containedEvidence("artifactManifest");
905
+ const recipe = recipeArtifact.document;
906
+ const resolution = resolutionArtifact.document;
907
+ const trace = traceArtifact.document;
908
+ const summary = summaryArtifact.document;
909
+ const artifactManifest = manifestArtifact.document;
910
+ const recipeDigest = `sha256:${normalizeRecipeDigest(digestRecipeDocument(recipe))}`;
911
+ const resolutionRoot = requireRecord(resolution.root, "funding balance evidence recipe resolution root");
912
+ if (resolutionRoot.digest !== recipeDigest || typeof resolutionRoot.ref !== "string" || resolutionRoot.ref.length === 0 || JSON.stringify(Object.keys(resolutionRoot).sort()) !== JSON.stringify(["digest", "ref"])) {
913
+ throw new Error("Funding balance evidence recipe resolution does not bind the emitted root recipe.");
914
+ }
915
+ if (!Array.isArray(resolution.dependencies) || !Array.isArray(resolution.edges)) {
916
+ throw new Error("Funding balance evidence recipe resolution graph is invalid.");
917
+ }
918
+ const resolvedRecipes = /* @__PURE__ */ new Map();
919
+ for (const [dependencyIndex, value2] of resolution.dependencies.entries()) {
920
+ const dependency = requireRecord(value2, `funding balance evidence dependency[${dependencyIndex}]`);
921
+ if (dependency.ref === resolutionRoot.ref) {
922
+ throw new Error("Funding balance evidence recipe dependency ref shadows the root ref.");
923
+ }
924
+ const ref = requireIdentifier(dependency.ref, `funding balance evidence dependency[${dependencyIndex}].ref`);
925
+ const digest = String(dependency.digest ?? "");
926
+ if (!digest.startsWith("sha256:") || !SHA256.test(digest.slice("sha256:".length)) || resolvedRecipes.has(ref)) {
927
+ throw new Error("Funding balance evidence recipe dependency identity is invalid.");
928
+ }
929
+ const expectedArtifact = `resolved-recipes/${digest.slice("sha256:".length)}.recipe.json`;
930
+ if (dependency.artifact !== expectedArtifact) {
931
+ throw new Error("Funding balance evidence recipe dependency artifact is invalid.");
932
+ }
933
+ const dependencyPath = await containedRegularPath(
934
+ releaseRoot,
935
+ path.relative(releaseRoot, path.resolve(path.dirname(recipeArtifact.artifactPath), expectedArtifact)),
936
+ `funding balance evidence dependency ${ref}`
937
+ );
938
+ const dependencyDocument = parseObject(await readFile(dependencyPath), `funding balance evidence dependency ${ref}`);
939
+ if (`sha256:${normalizeRecipeDigest(digestRecipeDocument(dependencyDocument))}` !== digest) {
940
+ throw new Error(`Funding balance evidence recipe dependency digest is invalid: ${ref}.`);
941
+ }
942
+ resolvedRecipes.set(ref, dependencyDocument);
943
+ }
944
+ const manifestArtifacts = Array.isArray(artifactManifest.artifacts) ? artifactManifest.artifacts : [];
945
+ for (const bound of [recipeArtifact, resolutionArtifact, traceArtifact, summaryArtifact]) {
946
+ const manifestPath = path.relative(path.dirname(manifestArtifact.artifactPath), bound.artifactPath).split(path.sep).join("/");
947
+ if (!manifestArtifacts.some((value2) => isRecord(value2) && value2.path === manifestPath)) {
948
+ throw new Error(`Funding balance artifact manifest does not bind ${manifestPath}.`);
949
+ }
950
+ }
951
+ for (const value2 of resolution.dependencies) {
952
+ const dependency = requireRecord(value2, "funding balance evidence dependency");
953
+ if (!manifestArtifacts.some((artifactValue) => isRecord(artifactValue) && artifactValue.path === dependency.artifact)) {
954
+ throw new Error(`Funding balance artifact manifest does not bind ${String(dependency.artifact)}.`);
955
+ }
956
+ }
957
+ const emittedNodes = validateResolvedTrace(recipe, resolution, resolvedRecipes, trace);
958
+ const wallet = successfulWalletOutput(
959
+ trace,
960
+ "funding balance evidence trace",
961
+ "ui-visible-session",
962
+ "extension-visible-wallet-session"
963
+ );
964
+ const runtimeSessionId = requireIdentifier(
965
+ evidenceDocument.runtimeSessionId,
966
+ "funding balance evidence runtimeSessionId"
967
+ );
968
+ if (evidenceDocument.selectedIdentitySha256 !== onboardingWallet.selectedIdentitySha256 || wallet.selectedIdentitySha256 !== onboardingWallet.selectedIdentitySha256 || wallet.runtimeSessionId !== runtimeSessionId || wallet.cdpPort !== onboardingWallet.cdpPort || extensionOrigin(wallet.targetUrl, "funding balance wallet") !== onboardingOrigin) {
969
+ throw new Error("Funding balance evidence wallet or runtime is not anchored to release onboarding.");
970
+ }
971
+ const currentRuntime = {
972
+ selectedIdentitySha256: String(wallet.selectedIdentitySha256),
973
+ runtimeSessionId,
974
+ cdpPort: Number(wallet.cdpPort),
975
+ cdpTargetId: String(wallet.cdpTargetId),
976
+ extensionOrigin: extensionOrigin(wallet.targetUrl, "funding balance wallet")
977
+ };
978
+ if (balanceRuntime && JSON.stringify(balanceRuntime) !== JSON.stringify(currentRuntime)) {
979
+ throw new Error("Funding balance evidence requirements do not share one wallet runtime identity.");
980
+ }
981
+ balanceRuntime = currentRuntime;
982
+ const visibleState = requireRecord(evidenceDocument.visibleState, "funding balance evidence visibleState");
983
+ if (visibleState.perpsAvailableMarginBaseUnits !== actualBaseUnits || visibleBalanceBaseUnits(trace, emittedNodes) !== actualBaseUnits) {
984
+ throw new Error("Funding balance actual value does not match its visible trace.");
985
+ }
986
+ if (!exactTerminalPass(recipe, trace, summary, artifactManifest)) {
987
+ throw new Error("Funding balance evidence trace is not exact terminal-pass.");
988
+ }
989
+ requirements.push({ kind, id, minBaseUnits: trusted.minBaseUnits, actualBaseUnits, evidenceSha256 });
990
+ }
991
+ if (!balanceRuntime) throw new Error("Funding balance proof has no runtime-bound requirement evidence.");
992
+ return {
993
+ requirements,
994
+ runtime: balanceRuntime,
995
+ releaseTreeSha256: String(artifact.treeSha256)
996
+ };
997
+ }
998
+ function releaseRuntimeValidator(projectRoot) {
999
+ return async (requireLive, cdpPort) => {
1000
+ const { extensionRuntimeReusable } = await import("./commands/launch/extension.js");
1001
+ const { releaseArtifactState, runtimeDistCheck } = await import("./adapters/extension/runtime-decision.js");
1002
+ const artifact = releaseArtifactState(projectRoot);
1003
+ if (artifact.status !== "valid" || runtimeDistCheck(projectRoot, artifact).status !== "fresh" || !artifact.sha256 || !artifact.treeSha256) {
1004
+ throw new Error("Runner-owned current release artifact is invalid or stale.");
1005
+ }
1006
+ if (requireLive) {
1007
+ const configuredPort = Number(process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
1008
+ if (configuredPort !== cdpPort || !await extensionRuntimeReusable(projectRoot, artifact)) {
1009
+ throw new Error("Runner-owned current release runtime does not own the funded CDP session.");
1010
+ }
1011
+ }
1012
+ return { artifactSha256: artifact.sha256, treeSha256: artifact.treeSha256 };
1013
+ };
1014
+ }
1015
+ function trustedFundingRequirements(identity) {
1016
+ const registry = TRUSTED_FUNDING_REGISTRIES[identity.registrySha256];
1017
+ const profile = registry?.[identity.profileId];
1018
+ const domain = profile?.[identity.domain];
1019
+ if (!domain) throw new Error("Funding requirements are absent from the runner-trusted registry.");
1020
+ return domain.requirements;
1021
+ }
1022
+ async function containedRegularPath(root, relative, label) {
1023
+ if (typeof relative !== "string" || !relative || path.isAbsolute(relative)) {
1024
+ throw new Error(`${label} must be a contained relative path.`);
1025
+ }
1026
+ const resolved = path.resolve(root, relative);
1027
+ if (!resolved.startsWith(`${root}${path.sep}`)) throw new Error(`${label} escapes the release root.`);
1028
+ const info = await lstat(resolved);
1029
+ if (!info.isFile() || info.isSymbolicLink() || await realpath(resolved) !== resolved || info.size <= 0 || info.size > MAX_DOCUMENT_BYTES) {
1030
+ throw new Error(`${label} must be a regular bounded non-symlink file.`);
1031
+ }
1032
+ return resolved;
1033
+ }
1034
+ function successfulWalletOutput(trace, label, method, proofPath) {
1035
+ const entries = Array.isArray(trace.entries) ? trace.entries : [];
1036
+ const wallets = entries.filter((entry) => isRecord(entry) && entry.action === "metamask.wallet.import" && entry.ok === true && isRecord(entry.output));
1037
+ if (wallets.length !== 1) throw new Error(`${label} must contain one successful visible wallet proof.`);
1038
+ const output = wallets[0].output;
1039
+ if (output.action !== "metamask.wallet.import" || output.method !== method || output.credentialSource !== "environment" || output.credentialSourceName !== "MM_RELEASE_SELECTED_WALLET_SRP" || output.proofPath !== proofPath || output.redacted !== true || output.selectedAddressMatched !== true || typeof output.selectedIdentitySha256 !== "string" || !SHA256.test(output.selectedIdentitySha256) || typeof output.runtimeSessionId !== "string" || !IDENTIFIER.test(output.runtimeSessionId) || !Number.isSafeInteger(output.cdpPort) || typeof output.cdpTargetId !== "string" || !IDENTIFIER.test(output.cdpTargetId)) {
1040
+ throw new Error(`${label} visible wallet proof is invalid.`);
1041
+ }
1042
+ return output;
1043
+ }
1044
+ function extensionOrigin(value, label) {
1045
+ try {
1046
+ const parsed = new URL(String(value));
1047
+ const origin = `${parsed.protocol}//${parsed.hostname}`;
1048
+ if (!ORIGIN.test(origin)) throw new Error("invalid origin");
1049
+ return origin;
1050
+ } catch {
1051
+ throw new Error(`${label} Extension origin is invalid.`);
1052
+ }
1053
+ }
1054
+ function visibleBalanceBaseUnits(trace, emittedNodes) {
1055
+ const entries = Array.isArray(trace.entries) ? trace.entries : [];
1056
+ const matches = entries.filter((entry) => isRecord(entry) && entry.action === "metamask.perps.read_visible_state" && entry.ok === true && isRecord(entry.output) && entry.output.action === entry.action && entry.output.surface === "home" && entry.output.source === "visible-ui" && isRecord(entry.output.balance) && entry.output.balance.visible === true && isRecord(emittedNodes.get(String(entry.nodeId))) && emittedNodes.get(String(entry.nodeId))?.surface === "home");
1057
+ const matchEntry = matches.at(-1);
1058
+ if (!matchEntry) throw new Error("Funding balance evidence requires a visible Perps Home balance observation.");
1059
+ const label = String(matchEntry.output.balance.label ?? "");
1060
+ const match = /(?:^|\s)\$([0-9]+(?:\.[0-9]{1,2})?)(?:\s|$)/u.exec(label);
1061
+ if (!match) throw new Error("Funding balance evidence visible amount is invalid.");
1062
+ const [whole, fraction = ""] = match[1].split(".");
1063
+ return `${BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"))}`;
1064
+ }
1065
+ function workflowGraph(recipe, label) {
1066
+ const workflow = requireRecord(recipe.workflow, `${label} workflow`);
1067
+ const nodes = requireRecord(workflow.nodes, `${label} workflow nodes`);
1068
+ const entry = requireIdentifier(workflow.entry, `${label} workflow entry`);
1069
+ if (!isRecord(nodes[entry])) throw new Error(`${label} workflow entry is absent.`);
1070
+ const reachable = /* @__PURE__ */ new Set();
1071
+ const pending = [entry];
1072
+ while (pending.length > 0) {
1073
+ const nodeId = pending.pop();
1074
+ if (reachable.has(nodeId)) continue;
1075
+ reachable.add(nodeId);
1076
+ const node = requireRecord(nodes[nodeId], `${label} node ${nodeId}`);
1077
+ requireIdentifier(node.action, `${label} node ${nodeId} action`);
1078
+ const cases = isRecord(node.cases) ? Object.values(node.cases) : [];
1079
+ for (const successor of [node.next, node.default, ...cases]) {
1080
+ if (successor === void 0) continue;
1081
+ if (typeof successor !== "string" || !isRecord(nodes[successor])) {
1082
+ throw new Error(`${label} node ${nodeId} has an invalid successor.`);
1083
+ }
1084
+ if (!reachable.has(successor)) pending.push(successor);
1085
+ }
1086
+ }
1087
+ return { entry, nodes, reachable };
1088
+ }
1089
+ function validateResolvedTrace(rootRecipe, resolution, dependencies, trace) {
1090
+ const resolutionRoot = requireRecord(resolution.root, "funding balance evidence resolution root");
1091
+ const rootRef = String(resolutionRoot.ref ?? "");
1092
+ if (!rootRef || !Array.isArray(resolution.edges)) {
1093
+ throw new Error("Funding balance evidence recipe resolution graph is invalid.");
1094
+ }
1095
+ if (dependencies.has(rootRef)) {
1096
+ throw new Error("Funding balance evidence recipe dependency ref shadows the root ref.");
1097
+ }
1098
+ const documents = new Map([[rootRef, rootRecipe], ...dependencies]);
1099
+ const expectedEdges = /* @__PURE__ */ new Set();
1100
+ for (const [source, document] of documents) {
1101
+ const graph = workflowGraph(document, `funding balance evidence recipe ${source}`);
1102
+ for (const nodeId of graph.reachable) {
1103
+ const node = requireRecord(graph.nodes[nodeId], `funding balance evidence recipe node ${nodeId}`);
1104
+ if (node.action !== "call") continue;
1105
+ const ref = requireIdentifier(node.ref, `funding balance evidence call ${nodeId} ref`);
1106
+ if (!dependencies.has(ref)) throw new Error(`Funding balance evidence recipe references unresolved dependency ${ref}.`);
1107
+ expectedEdges.add(`${source}\0${ref}`);
1108
+ }
1109
+ }
1110
+ const actualEdges = /* @__PURE__ */ new Set();
1111
+ for (const value of resolution.edges) {
1112
+ const edge = requireRecord(value, "funding balance evidence recipe resolution edge");
1113
+ if (JSON.stringify(Object.keys(edge).sort()) !== JSON.stringify(["from", "to"])) {
1114
+ throw new Error("Funding balance evidence recipe resolution edge fields are invalid.");
1115
+ }
1116
+ const from = String(edge.from ?? "");
1117
+ const to = String(edge.to ?? "");
1118
+ const key = `${from}\0${to}`;
1119
+ if (!documents.has(from) || !dependencies.has(to) || actualEdges.has(key)) {
1120
+ throw new Error("Funding balance evidence recipe resolution edge is invalid.");
1121
+ }
1122
+ actualEdges.add(key);
1123
+ }
1124
+ if (JSON.stringify([...actualEdges].sort()) !== JSON.stringify([...expectedEdges].sort())) {
1125
+ throw new Error("Funding balance evidence recipe resolution edges do not match the emitted call graph.");
1126
+ }
1127
+ const reachedDocuments = /* @__PURE__ */ new Set();
1128
+ const visiting = /* @__PURE__ */ new Set();
1129
+ const visitDocument = (ref) => {
1130
+ if (visiting.has(ref)) throw new Error("Funding balance evidence recipe call graph contains a cycle.");
1131
+ if (reachedDocuments.has(ref)) return;
1132
+ visiting.add(ref);
1133
+ reachedDocuments.add(ref);
1134
+ for (const key of expectedEdges) {
1135
+ const [from, to] = key.split("\0");
1136
+ if (from === ref) visitDocument(to);
1137
+ }
1138
+ visiting.delete(ref);
1139
+ };
1140
+ visitDocument(rootRef);
1141
+ if (reachedDocuments.size !== documents.size) {
1142
+ throw new Error("Funding balance evidence recipe resolution contains unreachable dependencies.");
1143
+ }
1144
+ const expectedNodes = /* @__PURE__ */ new Map();
1145
+ const transitions = /* @__PURE__ */ new Map();
1146
+ const addTransition = (from, to) => {
1147
+ const successors = transitions.get(from) ?? /* @__PURE__ */ new Set();
1148
+ successors.add(to);
1149
+ transitions.set(from, successors);
1150
+ };
1151
+ const expand = (ref, prefix, stack) => {
1152
+ if (stack.includes(ref)) throw new Error("Funding balance evidence recipe call graph contains a cycle.");
1153
+ const document = documents.get(ref);
1154
+ if (!document) throw new Error(`Funding balance evidence recipe dependency is absent: ${ref}.`);
1155
+ const graph = workflowGraph(document, `funding balance evidence recipe ${ref}`);
1156
+ const nested = /* @__PURE__ */ new Map();
1157
+ for (const nodeId of graph.reachable) {
1158
+ const node = requireRecord(graph.nodes[nodeId], `funding balance evidence recipe node ${nodeId}`);
1159
+ const traceNodeId = `${prefix}${nodeId}`;
1160
+ if (!NODE_ID.test(traceNodeId) || expectedNodes.has(traceNodeId)) {
1161
+ throw new Error("Funding balance evidence recipe expands invalid or duplicate trace node IDs.");
1162
+ }
1163
+ const action = String(node.action);
1164
+ if (!FUNDING_BALANCE_READ_ONLY_ACTIONS.has(action)) {
1165
+ throw new Error(`Funding balance evidence graph contains non-read-only action ${action}.`);
1166
+ }
1167
+ expectedNodes.set(traceNodeId, node);
1168
+ if (node.action === "call") {
1169
+ nested.set(nodeId, expand(String(node.ref), `${traceNodeId}/`, [...stack, ref]));
1170
+ }
1171
+ }
1172
+ const entryFor = (nodeId) => nested.get(nodeId)?.entry ?? `${prefix}${nodeId}`;
1173
+ const terminals = /* @__PURE__ */ new Set();
1174
+ for (const nodeId of graph.reachable) {
1175
+ const node = requireRecord(graph.nodes[nodeId], `funding balance evidence recipe node ${nodeId}`);
1176
+ const traceNodeId = `${prefix}${nodeId}`;
1177
+ const called = nested.get(nodeId);
1178
+ if (called) for (const terminal2 of called.terminals) addTransition(terminal2, traceNodeId);
1179
+ const cases = isRecord(node.cases) ? Object.values(node.cases) : [];
1180
+ const successors = [node.next, node.default, ...cases].filter((value) => typeof value === "string");
1181
+ if (successors.length === 0) {
1182
+ if (node.action !== "end") throw new Error(`Funding balance evidence recipe node ${traceNodeId} has no terminal successor.`);
1183
+ terminals.add(traceNodeId);
1184
+ } else {
1185
+ for (const successor of successors) addTransition(traceNodeId, entryFor(successor));
1186
+ }
1187
+ }
1188
+ return { entry: entryFor(graph.entry), terminals };
1189
+ };
1190
+ const expanded = expand(rootRef, "", []);
1191
+ const entries = Array.isArray(trace.entries) ? trace.entries : [];
1192
+ if (entries.length === 0 || !isRecord(entries[0]) || entries[0].nodeId !== expanded.entry) {
1193
+ throw new Error("Funding balance evidence trace does not start at the emitted root entry.");
1194
+ }
1195
+ const seen = /* @__PURE__ */ new Set();
1196
+ for (const [index, value] of entries.entries()) {
1197
+ const entry = requireRecord(value, `funding balance evidence trace entries[${index}]`);
1198
+ const nodeId = String(entry.nodeId ?? "");
1199
+ if (entry.ok !== true || seen.has(nodeId) || expectedNodes.get(nodeId)?.action !== entry.action) {
1200
+ throw new Error("Funding balance evidence trace entries do not match unique successful emitted nodes and actions.");
1201
+ }
1202
+ seen.add(nodeId);
1203
+ if (index > 0) {
1204
+ const previous = requireRecord(entries[index - 1], `funding balance evidence trace entries[${index - 1}]`);
1205
+ if (!transitions.get(String(previous.nodeId))?.has(nodeId)) {
1206
+ throw new Error("Funding balance evidence trace execution order does not match the emitted recipe graph.");
1207
+ }
1208
+ }
1209
+ }
1210
+ const terminal = requireRecord(entries.at(-1), "funding balance evidence trace terminal");
1211
+ const rootGraph = workflowGraph(rootRecipe, "funding balance evidence root recipe");
1212
+ const rootNode = isRecord(rootGraph.nodes[String(terminal.nodeId)]) ? rootGraph.nodes[String(terminal.nodeId)] : void 0;
1213
+ if (!expanded.terminals.has(String(terminal.nodeId)) || rootNode?.action !== "end" || rootNode.status !== "pass" || terminal.action !== "end" || terminal.status !== "pass") {
1214
+ throw new Error("Funding balance evidence trace lacks an exact reachable root PASS terminal.");
1215
+ }
1216
+ return expectedNodes;
1217
+ }
1218
+ function reachableRootPassEnds(recipe) {
1219
+ if (!isRecord(recipe.workflow)) return /* @__PURE__ */ new Set();
1220
+ const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
1221
+ const entry = recipe.workflow.entry;
1222
+ if (!nodes || typeof entry !== "string" || !isRecord(nodes[entry])) return /* @__PURE__ */ new Set();
1223
+ const reachable = /* @__PURE__ */ new Set();
1224
+ const passEnds = /* @__PURE__ */ new Set();
1225
+ const pending = [entry];
1226
+ while (pending.length > 0) {
1227
+ const nodeId = pending.pop();
1228
+ if (reachable.has(nodeId)) continue;
1229
+ reachable.add(nodeId);
1230
+ const node = nodes[nodeId];
1231
+ if (!isRecord(node)) continue;
1232
+ if (node.action === "end" && node.status === "pass") passEnds.add(nodeId);
1233
+ const cases = isRecord(node.cases) ? Object.values(node.cases) : [];
1234
+ for (const successor of [node.next, node.default, ...cases]) {
1235
+ if (typeof successor === "string" && isRecord(nodes[successor]) && !reachable.has(successor)) pending.push(successor);
1236
+ }
1237
+ }
1238
+ return passEnds;
1239
+ }
1240
+ function exactTerminalPass(recipe, trace, summary, artifactManifest) {
1241
+ if (!Array.isArray(trace.entries) || trace.entries.length === 0) return false;
1242
+ const terminal = trace.entries.at(-1);
1243
+ return isRecord(terminal) && terminal.action === "end" && terminal.ok === true && terminal.status === "pass" && typeof terminal.nodeId === "string" && IDENTIFIER.test(terminal.nodeId) && !terminal.nodeId.includes("/") && reachableRootPassEnds(recipe).has(terminal.nodeId) && summary.status === "pass" && summary.failed === 0 && summary.total === trace.entries.length && summary.passed === trace.entries.length && artifactManifest.runStatus === "pass";
1244
+ }
1245
+ function parsePreflightRequirements(value) {
1246
+ if (!Array.isArray(value)) throw new Error("Funding preflight requirements must be an array.");
1247
+ return value.map((entry, index) => {
1248
+ const record = requireRecord(entry, `funding preflight requirements[${index}]`);
1249
+ return {
1250
+ kind: requireIdentifier(record.kind, `funding preflight requirements[${index}].kind`),
1251
+ id: requireIdentifier(record.id, `funding preflight requirements[${index}].id`),
1252
+ minBaseUnits: requireInteger(record.minBaseUnits, `funding preflight requirements[${index}].minBaseUnits`),
1253
+ actualBaseUnits: requireInteger(record.actualBaseUnits, `funding preflight requirements[${index}].actualBaseUnits`),
1254
+ evidenceSha256: requireIdentifier(
1255
+ record.evidenceSha256,
1256
+ `funding preflight requirements[${index}].evidenceSha256`,
1257
+ true
1258
+ )
1259
+ };
1260
+ });
1261
+ }
1262
+ function validateTrustedFundingRegistry(identity, operations, budgets) {
1263
+ const registry = TRUSTED_FUNDING_REGISTRIES[identity.registrySha256];
1264
+ const profile = registry?.[identity.profileId];
1265
+ const domain = profile?.[identity.domain];
1266
+ if (!domain || !domain.clients.has(identity.client)) {
1267
+ throw new Error("Funding identity is not present in the runner-trusted funding registry.");
1268
+ }
1269
+ const suppliedBudgets = new Map(budgets.map((budget) => [
1270
+ `${budget.operation}:${budget.unit}`,
1271
+ budget
1272
+ ]));
1273
+ const requested = /* @__PURE__ */ new Map();
1274
+ for (const operation of operations) {
1275
+ const key = `${operation.operation}:${operation.unit}`;
1276
+ const amountBaseUnits = BigInt(operation.amountBaseUnits);
1277
+ const aggregate = requested.get(key) ?? {
1278
+ operation: operation.operation,
1279
+ count: 0,
1280
+ amountBaseUnits: 0n,
1281
+ maxAmountBaseUnits: 0n
1282
+ };
1283
+ aggregate.count += operation.count;
1284
+ aggregate.amountBaseUnits += amountBaseUnits * BigInt(operation.count);
1285
+ if (amountBaseUnits > aggregate.maxAmountBaseUnits) aggregate.maxAmountBaseUnits = amountBaseUnits;
1286
+ requested.set(key, aggregate);
1287
+ }
1288
+ for (const [key, aggregate] of requested) {
1289
+ const trusted = domain.budgets.get(key);
1290
+ const supplied = suppliedBudgets.get(key);
1291
+ if (!trusted || !supplied || BigInt(supplied.maxBaseUnits) > BigInt(trusted.maxBaseUnits) || supplied.maxCount > trusted.maxCount || aggregate.maxAmountBaseUnits > BigInt(trusted.maxBaseUnits) || aggregate.count > trusted.maxCount || aggregate.amountBaseUnits > BigInt(trusted.maxBaseUnits) * BigInt(trusted.maxCount)) {
1292
+ throw new Error(`Funding operation ${aggregate.operation} is not bounded by the runner-trusted funding registry.`);
1293
+ }
1294
+ }
1295
+ }
1296
+ function parseOperations(value, label) {
1297
+ if (!Array.isArray(value) || value.length === 0 || value.length > 32) {
1298
+ throw new Error(`${label} must contain 1-32 entries.`);
1299
+ }
1300
+ const seen = /* @__PURE__ */ new Set();
1301
+ return value.map((entry, index) => {
1302
+ const record = requireRecord(entry, `${label}[${index}]`);
1303
+ const amountBaseUnits = requireInteger(record.amountBaseUnits, `${label}[${index}].amountBaseUnits`);
1304
+ const count = requireCount(record.count, `${label}[${index}].count`);
1305
+ const operation = requireIdentifier(record.operation, `${label}[${index}].operation`);
1306
+ const unit = requireIdentifier(record.unit, `${label}[${index}].unit`);
1307
+ const key = `${operation}:${unit}`;
1308
+ if (seen.has(key)) throw new Error(`${label} contains a duplicate operation/unit key.`);
1309
+ seen.add(key);
1310
+ return {
1311
+ operation,
1312
+ unit,
1313
+ amountBaseUnits,
1314
+ count
1315
+ };
1316
+ });
1317
+ }
1318
+ function parseBudgets(value) {
1319
+ if (!Array.isArray(value) || value.length === 0 || value.length > 64) {
1320
+ throw new Error("Funding preflight budgets must contain 1-64 entries.");
1321
+ }
1322
+ const seen = /* @__PURE__ */ new Set();
1323
+ return value.map((entry, index) => {
1324
+ const record = requireRecord(entry, `funding preflight budgets[${index}]`);
1325
+ const operation = requireIdentifier(record.operation, `funding preflight budgets[${index}].operation`);
1326
+ const unit = requireIdentifier(record.unit, `funding preflight budgets[${index}].unit`);
1327
+ const key = `${operation}:${unit}`;
1328
+ if (seen.has(key)) throw new Error("Funding preflight budgets contain a duplicate operation/unit key.");
1329
+ seen.add(key);
1330
+ return {
1331
+ operation,
1332
+ unit,
1333
+ maxBaseUnits: requireInteger(record.maxBaseUnits, `funding preflight budgets[${index}].maxBaseUnits`),
1334
+ maxCount: requireCount(record.maxCount, `funding preflight budgets[${index}].maxCount`)
1335
+ };
1336
+ });
1337
+ }
1338
+ function requireRecord(value, label) {
1339
+ if (!isRecord(value)) throw new Error(`${label} must be an object.`);
1340
+ return value;
1341
+ }
1342
+ function requireIdentifier(value, label, digest = false) {
1343
+ if (typeof value !== "string" || !(digest ? SHA256 : IDENTIFIER).test(value)) {
1344
+ throw new Error(`${label} is invalid.`);
1345
+ }
1346
+ return value;
1347
+ }
1348
+ function requireInteger(value, label) {
1349
+ if (typeof value !== "string" || !INTEGER.test(value)) throw new Error(`${label} must be a bounded integer string.`);
1350
+ return value;
1351
+ }
1352
+ function requireCount(value, label) {
1353
+ if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 1e3) {
1354
+ throw new Error(`${label} must be an integer from 1 through 1000.`);
1355
+ }
1356
+ return Number(value);
1357
+ }
1358
+ function normalizeRecipeDigest(value) {
1359
+ const digest = value.startsWith("sha256:") ? value.slice("sha256:".length) : value;
1360
+ if (!SHA256.test(digest)) throw new Error("Recipe digest is invalid.");
1361
+ return digest;
1362
+ }
1363
+ function sha256(value) {
1364
+ return createHash("sha256").update(value).digest("hex");
1365
+ }
1366
+ function canonical(value) {
1367
+ return `${JSON.stringify(value)}
1368
+ `;
1369
+ }
1370
+ function isRecord(value) {
1371
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1372
+ }
1373
+ function deepFreeze(value) {
1374
+ if (value && typeof value === "object") {
1375
+ for (const child of Object.values(value)) deepFreeze(child);
1376
+ Object.freeze(value);
1377
+ }
1378
+ return value;
1379
+ }
1380
+ export {
1381
+ authorizeTrustedMutationPlan,
1382
+ loadTrustedMutationBase,
1383
+ selectedIdentityFromWalletImport,
1384
+ serializeTrustedMutationContext,
1385
+ trustedMutationForAction
1386
+ };