@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
@@ -37,6 +37,10 @@ import { acquireCheckoutLock } from "../../checkout-lock.js";
37
37
  import { JsonStreamWriter } from "../../json-stream.js";
38
38
  import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
39
39
  import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
40
+ import {
41
+ releaseArtifactState,
42
+ runtimeDistCheck
43
+ } from "../../adapters/extension/runtime-decision.js";
40
44
  const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
41
45
  "build",
42
46
  "watch",
@@ -124,13 +128,41 @@ async function handleLaunchLocked(argv, stream) {
124
128
  const wantWatch = flag(options, "watch");
125
129
  const wantRunway = flag(options, "runway");
126
130
  const wantClearMetro = flag(options, "clearMetro");
131
+ const requestedSurface = str(options, "surface");
132
+ const legacySidepanel = flag(options, "sidepanel");
133
+ const legacyFullscreen = flag(options, "fullscreen");
127
134
  if (wantRunway && adapter !== "mobile") {
128
135
  return launchUsage(jsonOutput, stream, "runway is mobile-only.", "drop --runway for the extension");
129
136
  }
130
137
  if (wantClearMetro && adapter !== "mobile") {
131
138
  return launchUsage(jsonOutput, stream, "--clear-metro is mobile-only.", "drop --clear-metro for the extension");
132
139
  }
133
- const displayMode = flag(options, "sidepanel") && !flag(options, "fullscreen") ? "sidepanel" : "fullscreen";
140
+ if (requestedSurface !== void 0 && requestedSurface !== "fullscreen" && requestedSurface !== "sidepanel") {
141
+ return launchUsage(
142
+ jsonOutput,
143
+ stream,
144
+ `--surface must be fullscreen or sidepanel (got: ${requestedSurface}).`,
145
+ "pass --surface fullscreen or --surface sidepanel"
146
+ );
147
+ }
148
+ if (legacySidepanel && legacyFullscreen) {
149
+ return launchUsage(
150
+ jsonOutput,
151
+ stream,
152
+ "--sidepanel and --fullscreen conflict.",
153
+ "pass exactly one Extension surface"
154
+ );
155
+ }
156
+ const legacySurface = legacySidepanel ? "sidepanel" : legacyFullscreen ? "fullscreen" : void 0;
157
+ if (requestedSurface && legacySurface && requestedSurface !== legacySurface) {
158
+ return launchUsage(
159
+ jsonOutput,
160
+ stream,
161
+ `--surface ${requestedSurface} conflicts with --${legacySurface}.`,
162
+ "pass one consistent Extension surface"
163
+ );
164
+ }
165
+ const displayMode = requestedSurface ?? legacySurface ?? "fullscreen";
134
166
  for (const portFlag of ["cdpPort", "watcherPort"]) {
135
167
  const value = str(options, portFlag);
136
168
  if (value !== void 0 && !/^\d+$/u.test(value)) {
@@ -189,7 +221,11 @@ async function handleLaunchLocked(argv, stream) {
189
221
  }
190
222
  stream.phase("healthcheck");
191
223
  if (adapter === "extension") {
192
- const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
224
+ const artifact = releaseArtifactState(target);
225
+ const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target, artifact);
226
+ const verifiedReleaseReuse = artifact.status === "valid" && reusable;
227
+ const verifiedReleaseLaunch = !wantWatch && tier !== "build" && artifact.status === "valid" && runtimeDistCheck(target, artifact).status === "fresh";
228
+ const willBuild = !reusable;
193
229
  if (willBuild) {
194
230
  const productConfigBlock = extensionProductConfigBlock(target);
195
231
  if (productConfigBlock) {
@@ -202,7 +238,7 @@ async function handleLaunchLocked(argv, stream) {
202
238
  });
203
239
  }
204
240
  }
205
- const depsBlock = extensionDepsBlock(target);
241
+ const depsBlock = verifiedReleaseReuse || verifiedReleaseLaunch ? null : extensionDepsBlock(target);
206
242
  if (depsBlock) {
207
243
  if (heal === "off") {
208
244
  return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
@@ -498,6 +534,22 @@ async function executeComposition(adapter, mobileTarget, tier, wantWatch, target
498
534
  return launchExtension(target, tier, wantWatch, displayMode);
499
535
  }
500
536
  async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, sidepanelRecoveryAllowed = true) {
537
+ if (adapter === "extension" && displayMode === "fullscreen") {
538
+ const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
539
+ const sidepanelArgs = ["close"];
540
+ if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
541
+ const fullscreen = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
542
+ if (fullscreen.status !== 0) {
543
+ return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
544
+ code: "FULLSCREEN_OPEN_FAILED",
545
+ message: "app launched but restoring the fullscreen Extension surface failed.",
546
+ recoverable: false,
547
+ userAction: `bash adapters/extension/sidepanel-toggle.sh close --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
548
+ exitCode: EXIT.runtime,
549
+ originalError: fullscreen.output.trim() || void 0
550
+ });
551
+ }
552
+ }
501
553
  if (adapter === "extension" && displayMode === "sidepanel") {
502
554
  const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
503
555
  const sidepanelArgs = ["open"];
@@ -196,6 +196,7 @@ function parsePort(value, errorMessage) {
196
196
  }
197
197
  function runtimeOptionsFromCli(options) {
198
198
  const recordVideo = options.recordVideo;
199
+ const hud = optionString(options, "hud");
199
200
  const trust = explicitRecipeTrustOptions({
200
201
  sourceTrust: optionString(options, "sourceTrust"),
201
202
  sourceKind: optionString(options, "sourceKind"),
@@ -210,6 +211,11 @@ function runtimeOptionsFromCli(options) {
210
211
  slot: optionString(options, "slot"),
211
212
  validationRuntimeDir: optionString(options, "validationRuntimeDir"),
212
213
  recordVideo: recordVideo === "full-run" ? "full-run" : false,
214
+ autoHud: hud === "show" ? true : hud === "hide" ? false : void 0,
215
+ fundingRequest: optionString(options, "fundingRequest"),
216
+ fundingPreflight: optionString(options, "fundingPreflight"),
217
+ fundingReleaseProof: optionString(options, "fundingReleaseProof"),
218
+ fundingBalanceProof: optionString(options, "fundingBalanceProof"),
213
219
  ...trust
214
220
  };
215
221
  }
@@ -23,8 +23,15 @@ import {
23
23
  } from "../paths.js";
24
24
  import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
25
25
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
26
- import { validateMetaMaskActionInputs } from "../metamask-action-validation.js";
26
+ import {
27
+ resolveMetaMaskParameterValue,
28
+ validateMetaMaskActionInputs
29
+ } from "../metamask-action-validation.js";
27
30
  import { gitLibraryProvenance } from "../library-provenance.js";
31
+ import {
32
+ authorizeTrustedMutationPlan,
33
+ loadTrustedMutationBase
34
+ } from "../funding-execution-context.js";
28
35
  import {
29
36
  beginRunDiagnostics,
30
37
  finishRunDiagnostics
@@ -139,9 +146,18 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
139
146
  );
140
147
  const { createMetaMaskRunner } = await import("../runner.js");
141
148
  const recipeDocument = runtimeRecipeDocument(recipe, absoluteRecipePath);
149
+ const trustedMutation = await loadTrustedMutationBase({
150
+ fundingRequest: runtimeOptions.fundingRequest,
151
+ fundingPreflight: runtimeOptions.fundingPreflight,
152
+ fundingReleaseProof: runtimeOptions.fundingReleaseProof,
153
+ fundingBalanceProof: runtimeOptions.fundingBalanceProof,
154
+ artifactsDir: absoluteArtifactsDir,
155
+ projectRoot,
156
+ rootRecipe: recipeDocument
157
+ });
142
158
  const suppressHudForLifecycleTiming = adapter === "mobile" && mobileRecipeRequiresUninterruptedLifecycleTiming(recipeDocument);
143
159
  const suppressHudForOpaqueMobile = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
144
- const runner = await createMetaMaskRunner(adapter, manifest, {
160
+ const runnerOptions = {
145
161
  quietStdout: runtimeOptions.stdoutIsMachineContract === true,
146
162
  suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
147
163
  autoHud: suppressHudForLifecycleTiming || suppressHudForOpaqueMobile ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
@@ -151,28 +167,41 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
151
167
  // task-local adapter. Inherited gateway provenance remains authoritative,
152
168
  // so an untrusted worker cannot promote itself with CLI flags.
153
169
  trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
170
+ };
171
+ let runner = await createMetaMaskRunner(adapter, manifest, {
172
+ ...runnerOptions,
173
+ trustedMutation
154
174
  });
155
175
  const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
176
+ const runRequest = {
177
+ ...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
178
+ artifactsDir: absoluteArtifactsDir,
179
+ projectRoot,
180
+ inheritProcessEnv: false,
181
+ env: {
182
+ ...recipeProcessEnvironment(),
183
+ ...recipeRunEnv(adapter, runtimeOptions),
184
+ METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
185
+ },
186
+ recordVideo: useFramedExtensionRecording ? false : recordVideo,
187
+ ...trust,
188
+ ...librarySources ? { librarySources } : {},
189
+ adapter,
190
+ ...runtimeOptions.params ? { params: runtimeOptions.params } : {}
191
+ };
192
+ if (trustedMutation) {
193
+ const executionPlan = await runner.preflight(runRequest);
194
+ const authorizedMutation = await authorizeTrustedMutationPlan(trustedMutation, executionPlan);
195
+ runner = await createMetaMaskRunner(adapter, manifest, {
196
+ ...runnerOptions,
197
+ trustedMutation: authorizedMutation
198
+ });
199
+ }
156
200
  return {
157
201
  runner,
158
202
  absoluteArtifactsDir,
159
203
  useFramedExtensionRecording,
160
- runRequest: {
161
- ...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
162
- artifactsDir: absoluteArtifactsDir,
163
- projectRoot,
164
- inheritProcessEnv: false,
165
- env: {
166
- ...recipeProcessEnvironment(),
167
- ...recipeRunEnv(adapter, runtimeOptions),
168
- METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
169
- },
170
- recordVideo: useFramedExtensionRecording ? false : recordVideo,
171
- ...trust,
172
- ...librarySources ? { librarySources } : {},
173
- adapter,
174
- ...runtimeOptions.params ? { params: runtimeOptions.params } : {}
175
- }
204
+ runRequest
176
205
  };
177
206
  }
178
207
  function runtimeRecipeDocument(recipe, absoluteRecipePath) {
@@ -201,8 +230,7 @@ function recipeProcessEnvironment() {
201
230
  }
202
231
  function executionLibrarySources(sources, invocationTrust) {
203
232
  return sources?.map((source) => {
204
- if (source.name === "metamask" || source.provenance?.trust === "trusted")
205
- return source;
233
+ if (source.name === "metamask") return source;
206
234
  return {
207
235
  ...source,
208
236
  provenance: {
@@ -347,7 +375,7 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
347
375
  );
348
376
  const findings = [
349
377
  ...withManifest.findings,
350
- ...validateMetaMaskActionInputs(recipe, adapter)
378
+ ...validateMetaMaskActionInputs(recipe, adapter, params)
351
379
  ];
352
380
  if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
353
381
  try {
@@ -376,10 +404,26 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
376
404
  dependency.document,
377
405
  manifest,
378
406
  validationOptions
379
- ).findings,
380
- ...validateMetaMaskActionInputs(dependency.document, adapter)
407
+ ).findings
381
408
  );
382
409
  }
410
+ const validateDependencyInstances = (document, parentParams) => {
411
+ if (!isRecord(document) || !isRecord(document.workflow) || !isRecord(document.workflow.nodes)) return;
412
+ for (const rawNode of Object.values(document.workflow.nodes)) {
413
+ const node = resolveMetaMaskParameterValue(rawNode, parentParams);
414
+ if (!isRecord(node) || node.action !== "call" || typeof node.ref !== "string") continue;
415
+ const dependency = dependencies.recipes.get(protocol.normalizeRecipeRef(node.ref));
416
+ if (!dependency) continue;
417
+ const supplied = isRecord(node.params) ? node.params : {};
418
+ const childParams = protocol.applyRecipeParamDefaults(
419
+ supplied,
420
+ isRecord(dependency.document) ? dependency.document.paramsSchema : void 0
421
+ );
422
+ findings.push(...validateMetaMaskActionInputs(dependency.document, adapter, childParams));
423
+ validateDependencyInstances(dependency.document, childParams);
424
+ }
425
+ };
426
+ validateDependencyInstances(recipe, params ?? {});
383
427
  } catch (error) {
384
428
  const resolutionError = error instanceof harness.RecipeResolutionError ? error : void 0;
385
429
  if (!resolutionError) throw error;
@@ -429,13 +473,19 @@ function effectiveLibrarySources(librarySources) {
429
473
  }
430
474
  async function listRunnableRecipes(adapter, librarySources) {
431
475
  const harness = await importRecipeHarness();
476
+ const sources = effectiveLibrarySources(librarySources);
432
477
  const resolution = await harness.loadRecipeLibraries(
433
- effectiveLibrarySources(librarySources),
478
+ sources,
434
479
  {
435
480
  adapter
436
481
  }
437
482
  );
438
- return [...resolution.recipes.values()].map(
483
+ const protocol = await importRecipeProtocol();
484
+ const { manifest } = await resolveActionManifest(adapter, void 0, sources);
485
+ const externalRecipeIds = new Set(resolution.recipes.keys());
486
+ return [...resolution.recipes.values()].filter((recipe) => protocol.validateRecipeWithManifest(recipe.document, manifest, {
487
+ externalRecipeIds
488
+ }).status === "valid").map(
439
489
  (recipe) => recipeSummary(recipe.ref, adapter, recipe.document, recipe)
440
490
  ).sort((left, right) => left.name.localeCompare(right.name));
441
491
  }
@@ -630,20 +680,6 @@ async function validateRunRecipeStatic(recipeArg, adapter, options, params = {})
630
680
  findings.push(
631
681
  ...protocol.validateRecipeParams(effectiveParams, recipe.paramsSchema).findings
632
682
  );
633
- const workflow = isRecord(recipe.workflow) ? recipe.workflow : void 0;
634
- const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
635
- if (adapter === "extension" && nodes) {
636
- for (const [nodeId, node] of Object.entries(nodes)) {
637
- if (isRecord(node) && node.action === "metamask.perps.start_state" && node.page === false) {
638
- findings.push({
639
- severity: "error",
640
- code: "recipe.extension-perps-start-page",
641
- path: `workflow.nodes.${nodeId}.page`,
642
- message: "Extension metamask.perps.start_state must enter Perps; omit page or provide a string target instead of false"
643
- });
644
- }
645
- }
646
- }
647
683
  }
648
684
  const validation = manifestOk ? await validateRecipeAdapterAware(
649
685
  adapter,