@mysten-incubation/devstack 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/build-integrations/vite/index.mjs +1 -0
  2. package/dist/build-integrations/vite/index.mjs.map +1 -1
  3. package/dist/cli/wirings/apply.mjs +24 -6
  4. package/dist/cli/wirings/apply.mjs.map +1 -1
  5. package/dist/cli/wirings/dump-deployment.mjs +2 -1
  6. package/dist/cli/wirings/dump-deployment.mjs.map +1 -1
  7. package/dist/cli/wirings/identity.mjs +2 -1
  8. package/dist/cli/wirings/identity.mjs.map +1 -1
  9. package/dist/cli/wirings/snapshot.mjs +2 -1
  10. package/dist/cli/wirings/snapshot.mjs.map +1 -1
  11. package/dist/cli/wirings/up.mjs +25 -29
  12. package/dist/cli/wirings/up.mjs.map +1 -1
  13. package/dist/plugins/package/codegen.mjs +4 -4
  14. package/dist/plugins/package/codegen.mjs.map +1 -1
  15. package/dist/plugins/package/dep-resolution.mjs +19 -9
  16. package/dist/plugins/package/dep-resolution.mjs.map +1 -1
  17. package/dist/plugins/package/index.d.mts +3 -3
  18. package/dist/plugins/package/index.mjs +9 -5
  19. package/dist/plugins/package/index.mjs.map +1 -1
  20. package/dist/plugins/package/mode-known.mjs +2 -2
  21. package/dist/plugins/package/mode-known.mjs.map +1 -1
  22. package/dist/plugins/package/mode-local.mjs +4 -4
  23. package/dist/plugins/package/mode-local.mjs.map +1 -1
  24. package/dist/plugins/sui/chain-build-container.mjs.map +1 -1
  25. package/dist/plugins/sui/index.d.mts +10 -10
  26. package/dist/plugins/sui/index.mjs.map +1 -1
  27. package/dist/plugins/sui/move/index.mjs +10 -6
  28. package/dist/plugins/sui/move/index.mjs.map +1 -1
  29. package/dist/plugins/wallet/codegen.d.mts +14 -10
  30. package/dist/plugins/wallet/codegen.mjs.map +1 -1
  31. package/dist/plugins/wallet/origin-policy.mjs +43 -3
  32. package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
  33. package/dist/plugins/wallet/service.mjs +9 -7
  34. package/dist/plugins/wallet/service.mjs.map +1 -1
  35. package/dist/plugins/walrus/client-services.mjs +13 -5
  36. package/dist/plugins/walrus/client-services.mjs.map +1 -1
  37. package/dist/plugins/walrus/codegen.d.mts +1 -0
  38. package/dist/plugins/walrus/codegen.mjs +1 -0
  39. package/dist/plugins/walrus/codegen.mjs.map +1 -1
  40. package/dist/plugins/walrus/errors.d.mts +1 -1
  41. package/dist/plugins/walrus/errors.mjs.map +1 -1
  42. package/dist/plugins/walrus/index.d.mts +10 -0
  43. package/dist/plugins/walrus/index.mjs +14 -3
  44. package/dist/plugins/walrus/index.mjs.map +1 -1
  45. package/dist/plugins/walrus/mode/known-deploy.d.mts +1 -0
  46. package/dist/plugins/walrus/mode/known-deploy.mjs +6 -2
  47. package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
  48. package/dist/plugins/walrus/mode/local-cluster.d.mts +6 -2
  49. package/dist/plugins/walrus/mode/local-cluster.mjs +10 -7
  50. package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
  51. package/dist/plugins/walrus/routable.mjs +27 -31
  52. package/dist/plugins/walrus/routable.mjs.map +1 -1
  53. package/dist/plugins/walrus/snapshot.mjs.map +1 -1
  54. package/dist/substrate/cross-process.mjs +2 -1
  55. package/dist/substrate/cross-process.mjs.map +1 -1
  56. package/dist/substrate/runtime/cross-process/roster.mjs +5 -2
  57. package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
  58. package/dist/surfaces/cli/commands/supervisor-presence.mjs +4 -2
  59. package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
  60. package/dist/surfaces/cli/errors.mjs +12 -2
  61. package/dist/surfaces/cli/errors.mjs.map +1 -1
  62. package/dist/surfaces/cli/index.mjs.map +1 -1
  63. package/images/walrus/Dockerfile +11 -7
  64. package/images/walrus/run-walrus-client-service.sh +41 -9
  65. package/package.json +1 -1
@@ -1,6 +1,38 @@
1
1
  import { LogAttr } from "../../substrate/runtime/observability/log-attrs.mjs";
2
+ import "../../substrate/runtime/routed-url.mjs";
2
3
  import { Effect } from "effect";
3
4
  //#region src/plugins/wallet/origin-policy.ts
5
+ const routedAppOriginScope = (inputs) => {
6
+ if (inputs.routedAppOrigin === null) return null;
7
+ try {
8
+ const url = new URL(inputs.routedAppOrigin);
9
+ const app = inputs.app.toLowerCase();
10
+ const stack = inputs.stack.toLowerCase();
11
+ const suffix = stack === "main" ? `${app}.localhost` : `${stack}.${app}.localhost`;
12
+ return {
13
+ protocol: url.protocol,
14
+ port: url.port,
15
+ app,
16
+ stack,
17
+ pattern: `${url.protocol}//*.${suffix}${url.port.length > 0 ? `:${url.port}` : ""}`
18
+ };
19
+ } catch {
20
+ return null;
21
+ }
22
+ };
23
+ const isSameStackRoutedOrigin = (scope, origin) => {
24
+ let url;
25
+ try {
26
+ url = new URL(origin);
27
+ } catch {
28
+ return false;
29
+ }
30
+ if (url.origin !== origin) return false;
31
+ if (url.protocol !== scope.protocol || url.port !== scope.port) return false;
32
+ const labels = url.hostname.toLowerCase().split(".");
33
+ if (scope.stack === "main") return labels.length === 3 && labels[0].length > 0 && labels[1] === scope.app && labels[2] === "localhost";
34
+ return labels.length === 4 && labels[0].length > 0 && labels[1] === scope.stack && labels[2] === scope.app && labels[3] === "localhost";
35
+ };
4
36
  /**
5
37
  * Resolve the per-stack origin allowlist.
6
38
  *
@@ -20,18 +52,26 @@ import { Effect } from "effect";
20
52
  */
21
53
  const resolveOriginPolicy = (inputs) => Effect.gen(function* () {
22
54
  const allowed = /* @__PURE__ */ new Set();
55
+ const routedScope = routedAppOriginScope(inputs);
23
56
  if (inputs.routedAppOrigin !== null) allowed.add(inputs.routedAppOrigin);
24
57
  for (const o of inputs.extraOrigins) allowed.add(o);
25
58
  if (allowed.size === 0) yield* Effect.logWarning("wallet origin allowlist is empty").pipe(Effect.annotateLogs({
26
59
  [LogAttr.app]: inputs.app,
27
60
  [LogAttr.stack]: inputs.stack
28
61
  }));
29
- return { allowed };
62
+ return {
63
+ allowed,
64
+ routedAppOriginPattern: routedScope?.pattern ?? null,
65
+ routedAppOriginScope: routedScope
66
+ };
30
67
  });
31
68
  const checkOrigin = (policy, headerValue) => {
32
69
  if (headerValue === void 0 || headerValue.length === 0) return "missing";
33
- return policy.allowed.has(headerValue) ? "ok" : "forbidden";
70
+ if (policy.allowed.has(headerValue)) return "ok";
71
+ if (policy.routedAppOriginScope !== null && isSameStackRoutedOrigin(policy.routedAppOriginScope, headerValue)) return "ok";
72
+ return "forbidden";
34
73
  };
74
+ const describeAllowedOrigins = (policy) => [...policy.allowed, ...policy.routedAppOriginPattern === null ? [] : [policy.routedAppOriginPattern]];
35
75
  /** Compose CORS headers for a successful request. Single-allowed-origin
36
76
  * echo (browsers don't honor wildcard with credentials, and we want
37
77
  * to keep `Access-Control-Allow-Credentials` open for fetch with
@@ -44,6 +84,6 @@ const corsHeadersFor = (origin) => ({
44
84
  vary: "origin"
45
85
  });
46
86
  //#endregion
47
- export { checkOrigin, corsHeadersFor, resolveOriginPolicy };
87
+ export { checkOrigin, corsHeadersFor, describeAllowedOrigins, resolveOriginPolicy };
48
88
 
49
89
  //# sourceMappingURL=origin-policy.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"origin-policy.mjs","names":[],"sources":["../../../src/plugins/wallet/origin-policy.ts"],"sourcesContent":["// Wallet plugin — CORS / origin allowlist policy.\n//\n// The allowlist is built from two real, substrate-wired sources: the\n// router-fronted dev-server origin for this stack (`routedAppOrigin`)\n// and any caller-supplied `extraOrigins`. Both are stack-scoped or\n// explicit, so neither opens the cross-stack pairing risk described in\n// 15-wallet.md.\n//\n// No raw-loopback origin: with the `hostService(...)` plugin devstack\n// owns the dev server's bind port (the broker allocates it; Vite binds\n// it via `--port {port}`), so the raw-loopback origin\n// (`http://127.0.0.1:<port>` / `http://localhost:<port>`) the dev sees\n// in Vite's own console banner is a real, knowable origin. The canonical\n// fix lives on the host-service side: it publishes the ROUTED origin as\n// its `HostServiceValue.url`, so `devstack up` output (and any consumer\n// holding the resolved host-service value) point devs at the URL whose\n// Origin THIS allowlist already accepts (the routed `routedAppOrigin`).\n// A raw-loopback allowlist branch would have to thread the host-service's\n// dynamic bind port into the wallet, which runs in the OPPOSITE direction\n// of the dependency edge (host-service `dependsOn` wallet), so the wallet\n// has no view of that port at its own boot. Devs who must load the raw\n// Vite URL pass it via `allowedOrigins` (→ `extraOrigins`).\n//\n// Why \"origin + bearer together\": bearer alone leaves a non-browser-\n// tooling bypass — curl / fetch from a service worker can forge\n// `Authorization` from a leaked token. Browsers always send `Origin`;\n// non-browsers omit it. Demanding BOTH closes the bypass.\n\nimport { Effect } from 'effect';\n\nimport { LogAttr } from '../../substrate/runtime/observability/log-attrs.ts';\n\n// ----------------------------------------------------------------------\n// Policy shape\n// ----------------------------------------------------------------------\n\n/** Result of resolving the origin allowlist at boot. Captured into\n * the HTTP handler closure so per-request checks are pure-string\n * comparison. */\nexport interface OriginPolicy {\n\treadonly allowed: ReadonlySet<string>;\n}\n\n/** Per-stack inputs the policy resolver needs. Supplied by the\n * substrate at acquire time (identity + routed-url derivation); this\n * module doesn't reach into the broker itself. */\nexport interface OriginPolicyInputs {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly routedAppOrigin: string | null;\n\treadonly extraOrigins: ReadonlyArray<string>;\n}\n\n// ----------------------------------------------------------------------\n// Resolution\n// ----------------------------------------------------------------------\n\n/**\n * Resolve the per-stack origin allowlist.\n *\n * - Always allowlisted: the router-fronted dev-server origin for this\n * stack (`routedAppOrigin`), when the router derivation produced one.\n * - Always allowlisted: any explicit caller-supplied origins from\n * `extraOrigins`.\n *\n * Empty-allowlist policy (no `routedAppOrigin` AND no `extraOrigins`):\n * allowed. The wallet boots normally; with an empty allowlist the\n * per-request gate refuses every request (every Origin lands in\n * `forbidden`). This is the correct behavior for a stack composed\n * without any client UI (e.g. node-only smoke / e2e configs) — the\n * wallet's keypair + token are still useful for the host process, but\n * the HTTP surface is effectively closed. A `Effect.logWarning`\n * surfaces the configuration for operator visibility.\n */\nexport const resolveOriginPolicy = (inputs: OriginPolicyInputs): Effect.Effect<OriginPolicy> =>\n\tEffect.gen(function* () {\n\t\tconst allowed = new Set<string>();\n\n\t\tif (inputs.routedAppOrigin !== null) {\n\t\t\tallowed.add(inputs.routedAppOrigin);\n\t\t}\n\n\t\tfor (const o of inputs.extraOrigins) {\n\t\t\tallowed.add(o);\n\t\t}\n\n\t\tif (allowed.size === 0) {\n\t\t\tyield* Effect.logWarning('wallet origin allowlist is empty').pipe(\n\t\t\t\tEffect.annotateLogs({\n\t\t\t\t\t[LogAttr.app]: inputs.app,\n\t\t\t\t\t[LogAttr.stack]: inputs.stack,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\treturn { allowed } satisfies OriginPolicy;\n\t});\n\n// ----------------------------------------------------------------------\n// Per-request check\n// ----------------------------------------------------------------------\n\n/** Per-request origin gate. Returns `'missing'` for absent Origin,\n * `'forbidden'` for Origin not in the allowlist, `'ok'` for accepted.\n *\n * Distilled-doc invariant (C12): missing Origin is its OWN refusal\n * shape (closes the curl / non-browser bypass — bearer alone is not\n * enough). */\nexport type OriginCheckResult = 'missing' | 'forbidden' | 'ok';\n\nexport const checkOrigin = (\n\tpolicy: OriginPolicy,\n\theaderValue: string | undefined,\n): OriginCheckResult => {\n\tif (headerValue === undefined || headerValue.length === 0) return 'missing';\n\treturn policy.allowed.has(headerValue) ? 'ok' : 'forbidden';\n};\n\n/** Compose CORS headers for a successful request. Single-allowed-origin\n * echo (browsers don't honor wildcard with credentials, and we want\n * to keep `Access-Control-Allow-Credentials` open for fetch with\n * `credentials: 'include'` if a future codegen seam needs it). */\nexport const corsHeadersFor = (origin: string): Readonly<Record<string, string>> => ({\n\t'access-control-allow-origin': origin,\n\t'access-control-allow-methods': 'GET, POST, OPTIONS',\n\t'access-control-allow-headers': 'authorization, content-type',\n\t'access-control-allow-credentials': 'true',\n\tvary: 'origin',\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0EA,MAAa,uBAAuB,WACnC,OAAO,IAAI,aAAa;CACvB,MAAM,0BAAU,IAAI,IAAY;CAEhC,IAAI,OAAO,oBAAoB,MAC9B,QAAQ,IAAI,OAAO,eAAe;CAGnC,KAAK,MAAM,KAAK,OAAO,cACtB,QAAQ,IAAI,CAAC;CAGd,IAAI,QAAQ,SAAS,GACpB,OAAO,OAAO,WAAW,kCAAkC,CAAC,CAAC,KAC5D,OAAO,aAAa;GAClB,QAAQ,MAAM,OAAO;GACrB,QAAQ,QAAQ,OAAO;CACzB,CAAC,CACF;CAGD,OAAO,EAAE,QAAQ;AAClB,CAAC;AAcF,MAAa,eACZ,QACA,gBACuB;CACvB,IAAI,gBAAgB,KAAA,KAAa,YAAY,WAAW,GAAG,OAAO;CAClE,OAAO,OAAO,QAAQ,IAAI,WAAW,IAAI,OAAO;AACjD;;;;;AAMA,MAAa,kBAAkB,YAAsD;CACpF,+BAA+B;CAC/B,gCAAgC;CAChC,gCAAgC;CAChC,oCAAoC;CACpC,MAAM;AACP"}
1
+ {"version":3,"file":"origin-policy.mjs","names":[],"sources":["../../../src/plugins/wallet/origin-policy.ts"],"sourcesContent":["// Wallet plugin — CORS / origin allowlist policy.\n//\n// The allowlist is built from two real, substrate-wired sources: the\n// router-fronted dev-server origin for this stack (`routedAppOrigin`,\n// generalized to same-stack routed endpoint origins) and any\n// caller-supplied `extraOrigins`. Both are stack-scoped or explicit, so\n// neither opens the cross-stack pairing risk described in 15-wallet.md.\n//\n// No raw-loopback origin: with the `hostService(...)` plugin devstack\n// owns the dev server's bind port (the broker allocates it; Vite binds\n// it via `--port {port}`), so the raw-loopback origin\n// (`http://127.0.0.1:<port>` / `http://localhost:<port>`) the dev sees\n// in Vite's own console banner is a real, knowable origin. The canonical\n// fix lives on the host-service side: it publishes the ROUTED origin as\n// its `HostServiceValue.url`, so `devstack up` output (and any consumer\n// holding the resolved host-service value) point devs at the URL whose\n// Origin THIS allowlist already accepts (the routed `routedAppOrigin`).\n// A raw-loopback allowlist branch would have to thread the host-service's\n// dynamic bind port into the wallet, which runs in the OPPOSITE direction\n// of the dependency edge (host-service `dependsOn` wallet), so the wallet\n// has no view of that port at its own boot. Devs who must load the raw\n// Vite URL pass it via `allowedOrigins` (→ `extraOrigins`).\n//\n// Why \"origin + bearer together\": bearer alone leaves a non-browser-\n// tooling bypass — curl / fetch from a service worker can forge\n// `Authorization` from a leaked token. Browsers always send `Origin`;\n// non-browsers omit it. Demanding BOTH closes the bypass.\n\nimport { Effect } from 'effect';\n\nimport { LogAttr } from '../../substrate/runtime/observability/log-attrs.ts';\nimport { DEFAULT_STACK } from '../../substrate/runtime/routed-url.ts';\n\n// ----------------------------------------------------------------------\n// Policy shape\n// ----------------------------------------------------------------------\n\n/** Result of resolving the origin allowlist at boot. Captured into\n * the HTTP handler closure so per-request checks are pure-string\n * comparison. */\nexport interface OriginPolicy {\n\treadonly allowed: ReadonlySet<string>;\n\treadonly routedAppOriginPattern: string | null;\n\treadonly routedAppOriginScope: RoutedAppOriginScope | null;\n}\n\n/** Per-stack inputs the policy resolver needs. Supplied by the\n * substrate at acquire time (identity + routed-url derivation); this\n * module doesn't reach into the broker itself. */\nexport interface OriginPolicyInputs {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly routedAppOrigin: string | null;\n\treadonly extraOrigins: ReadonlyArray<string>;\n}\n\nexport interface RoutedAppOriginScope {\n\treadonly protocol: string;\n\treadonly port: string;\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly pattern: string;\n}\n\nconst routedAppOriginScope = (inputs: OriginPolicyInputs): RoutedAppOriginScope | null => {\n\tif (inputs.routedAppOrigin === null) return null;\n\ttry {\n\t\tconst url = new URL(inputs.routedAppOrigin);\n\t\tconst app = inputs.app.toLowerCase();\n\t\tconst stack = inputs.stack.toLowerCase();\n\t\tconst suffix = stack === DEFAULT_STACK ? `${app}.localhost` : `${stack}.${app}.localhost`;\n\t\treturn {\n\t\t\tprotocol: url.protocol,\n\t\t\tport: url.port,\n\t\t\tapp,\n\t\t\tstack,\n\t\t\tpattern: `${url.protocol}//*.${suffix}${url.port.length > 0 ? `:${url.port}` : ''}`,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst isSameStackRoutedOrigin = (scope: RoutedAppOriginScope, origin: string): boolean => {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(origin);\n\t} catch {\n\t\treturn false;\n\t}\n\tif (url.origin !== origin) return false;\n\tif (url.protocol !== scope.protocol || url.port !== scope.port) return false;\n\n\tconst labels = url.hostname.toLowerCase().split('.');\n\tif (scope.stack === DEFAULT_STACK) {\n\t\treturn (\n\t\t\tlabels.length === 3 &&\n\t\t\tlabels[0]!.length > 0 &&\n\t\t\tlabels[1] === scope.app &&\n\t\t\tlabels[2] === 'localhost'\n\t\t);\n\t}\n\treturn (\n\t\tlabels.length === 4 &&\n\t\tlabels[0]!.length > 0 &&\n\t\tlabels[1] === scope.stack &&\n\t\tlabels[2] === scope.app &&\n\t\tlabels[3] === 'localhost'\n\t);\n};\n\n// ----------------------------------------------------------------------\n// Resolution\n// ----------------------------------------------------------------------\n\n/**\n * Resolve the per-stack origin allowlist.\n *\n * - Always allowlisted: the router-fronted dev-server origin for this\n * stack (`routedAppOrigin`), when the router derivation produced one.\n * - Always allowlisted: any explicit caller-supplied origins from\n * `extraOrigins`.\n *\n * Empty-allowlist policy (no `routedAppOrigin` AND no `extraOrigins`):\n * allowed. The wallet boots normally; with an empty allowlist the\n * per-request gate refuses every request (every Origin lands in\n * `forbidden`). This is the correct behavior for a stack composed\n * without any client UI (e.g. node-only smoke / e2e configs) — the\n * wallet's keypair + token are still useful for the host process, but\n * the HTTP surface is effectively closed. A `Effect.logWarning`\n * surfaces the configuration for operator visibility.\n */\nexport const resolveOriginPolicy = (inputs: OriginPolicyInputs): Effect.Effect<OriginPolicy> =>\n\tEffect.gen(function* () {\n\t\tconst allowed = new Set<string>();\n\t\tconst routedScope = routedAppOriginScope(inputs);\n\n\t\tif (inputs.routedAppOrigin !== null) {\n\t\t\tallowed.add(inputs.routedAppOrigin);\n\t\t}\n\n\t\tfor (const o of inputs.extraOrigins) {\n\t\t\tallowed.add(o);\n\t\t}\n\n\t\tif (allowed.size === 0) {\n\t\t\tyield* Effect.logWarning('wallet origin allowlist is empty').pipe(\n\t\t\t\tEffect.annotateLogs({\n\t\t\t\t\t[LogAttr.app]: inputs.app,\n\t\t\t\t\t[LogAttr.stack]: inputs.stack,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tallowed,\n\t\t\troutedAppOriginPattern: routedScope?.pattern ?? null,\n\t\t\troutedAppOriginScope: routedScope,\n\t\t} satisfies OriginPolicy;\n\t});\n\n// ----------------------------------------------------------------------\n// Per-request check\n// ----------------------------------------------------------------------\n\n/** Per-request origin gate. Returns `'missing'` for absent Origin,\n * `'forbidden'` for Origin not in the allowlist, `'ok'` for accepted.\n *\n * Distilled-doc invariant (C12): missing Origin is its OWN refusal\n * shape (closes the curl / non-browser bypass — bearer alone is not\n * enough). */\nexport type OriginCheckResult = 'missing' | 'forbidden' | 'ok';\n\nexport const checkOrigin = (\n\tpolicy: OriginPolicy,\n\theaderValue: string | undefined,\n): OriginCheckResult => {\n\tif (headerValue === undefined || headerValue.length === 0) return 'missing';\n\tif (policy.allowed.has(headerValue)) return 'ok';\n\tif (\n\t\tpolicy.routedAppOriginScope !== null &&\n\t\tisSameStackRoutedOrigin(policy.routedAppOriginScope, headerValue)\n\t) {\n\t\treturn 'ok';\n\t}\n\treturn 'forbidden';\n};\n\nexport const describeAllowedOrigins = (policy: OriginPolicy): ReadonlyArray<string> => [\n\t...policy.allowed,\n\t...(policy.routedAppOriginPattern === null ? [] : [policy.routedAppOriginPattern]),\n];\n\n/** Compose CORS headers for a successful request. Single-allowed-origin\n * echo (browsers don't honor wildcard with credentials, and we want\n * to keep `Access-Control-Allow-Credentials` open for fetch with\n * `credentials: 'include'` if a future codegen seam needs it). */\nexport const corsHeadersFor = (origin: string): Readonly<Record<string, string>> => ({\n\t'access-control-allow-origin': origin,\n\t'access-control-allow-methods': 'GET, POST, OPTIONS',\n\t'access-control-allow-headers': 'authorization, content-type',\n\t'access-control-allow-credentials': 'true',\n\tvary: 'origin',\n});\n"],"mappings":";;;;AAgEA,MAAM,wBAAwB,WAA4D;CACzF,IAAI,OAAO,oBAAoB,MAAM,OAAO;CAC5C,IAAI;EACH,MAAM,MAAM,IAAI,IAAI,OAAO,eAAe;EAC1C,MAAM,MAAM,OAAO,IAAI,YAAY;EACnC,MAAM,QAAQ,OAAO,MAAM,YAAY;EACvC,MAAM,SAAS,UAAA,SAA0B,GAAG,IAAI,cAAc,GAAG,MAAM,GAAG,IAAI;EAC9E,OAAO;GACN,UAAU,IAAI;GACd,MAAM,IAAI;GACV;GACA;GACA,SAAS,GAAG,IAAI,SAAS,MAAM,SAAS,IAAI,KAAK,SAAS,IAAI,IAAI,IAAI,SAAS;EAChF;CACD,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,2BAA2B,OAA6B,WAA4B;CACzF,IAAI;CACJ,IAAI;EACH,MAAM,IAAI,IAAI,MAAM;CACrB,QAAQ;EACP,OAAO;CACR;CACA,IAAI,IAAI,WAAW,QAAQ,OAAO;CAClC,IAAI,IAAI,aAAa,MAAM,YAAY,IAAI,SAAS,MAAM,MAAM,OAAO;CAEvE,MAAM,SAAS,IAAI,SAAS,YAAY,CAAC,CAAC,MAAM,GAAG;CACnD,IAAI,MAAM,UAAA,QACT,OACC,OAAO,WAAW,KAClB,OAAO,EAAE,CAAE,SAAS,KACpB,OAAO,OAAO,MAAM,OACpB,OAAO,OAAO;CAGhB,OACC,OAAO,WAAW,KAClB,OAAO,EAAE,CAAE,SAAS,KACpB,OAAO,OAAO,MAAM,SACpB,OAAO,OAAO,MAAM,OACpB,OAAO,OAAO;AAEhB;;;;;;;;;;;;;;;;;;AAuBA,MAAa,uBAAuB,WACnC,OAAO,IAAI,aAAa;CACvB,MAAM,0BAAU,IAAI,IAAY;CAChC,MAAM,cAAc,qBAAqB,MAAM;CAE/C,IAAI,OAAO,oBAAoB,MAC9B,QAAQ,IAAI,OAAO,eAAe;CAGnC,KAAK,MAAM,KAAK,OAAO,cACtB,QAAQ,IAAI,CAAC;CAGd,IAAI,QAAQ,SAAS,GACpB,OAAO,OAAO,WAAW,kCAAkC,CAAC,CAAC,KAC5D,OAAO,aAAa;GAClB,QAAQ,MAAM,OAAO;GACrB,QAAQ,QAAQ,OAAO;CACzB,CAAC,CACF;CAGD,OAAO;EACN;EACA,wBAAwB,aAAa,WAAW;EAChD,sBAAsB;CACvB;AACD,CAAC;AAcF,MAAa,eACZ,QACA,gBACuB;CACvB,IAAI,gBAAgB,KAAA,KAAa,YAAY,WAAW,GAAG,OAAO;CAClE,IAAI,OAAO,QAAQ,IAAI,WAAW,GAAG,OAAO;CAC5C,IACC,OAAO,yBAAyB,QAChC,wBAAwB,OAAO,sBAAsB,WAAW,GAEhE,OAAO;CAER,OAAO;AACR;AAEA,MAAa,0BAA0B,WAAgD,CACtF,GAAG,OAAO,SACV,GAAI,OAAO,2BAA2B,OAAO,CAAC,IAAI,CAAC,OAAO,sBAAsB,CACjF;;;;;AAMA,MAAa,kBAAkB,YAAsD;CACpF,+BAA+B;CAC/B,gCAAgC;CAChC,gCAAgC;CAChC,oCAAoC;CACpC,MAAM;AACP"}
@@ -1,6 +1,6 @@
1
1
  import { WalletHttpPath } from "../../contracts/wallet-protocol.mjs";
2
2
  import { walletBootError } from "./errors.mjs";
3
- import { resolveOriginPolicy } from "./origin-policy.mjs";
3
+ import { describeAllowedOrigins, resolveOriginPolicy } from "./origin-policy.mjs";
4
4
  import "./protocol.mjs";
5
5
  import { acquirePairingToken, composePairUrl, tokenPath } from "./pairing.mjs";
6
6
  import { startHttpServer } from "./server.mjs";
@@ -46,16 +46,17 @@ const acquireWallet = (opts, ctx) => Effect.gen(function* () {
46
46
  const bindAddress = opts.bindAddress ?? WALLET_DEFAULT_BIND_ADDRESS;
47
47
  const port = yield* ctx.allocatePort(opts.port, portProbeHostForBindAddress(bindAddress));
48
48
  const token = yield* acquirePairingToken(tokenPath(ctx.stateRoot));
49
+ const policy = yield* resolveOriginPolicy({
50
+ app: ctx.app,
51
+ stack: ctx.stack,
52
+ routedAppOrigin: ctx.routedAppOrigin,
53
+ extraOrigins: opts.allowedOrigins ?? []
54
+ });
49
55
  const server = yield* startHttpServer({
50
56
  bindAddress,
51
57
  port,
52
58
  token,
53
- policy: yield* resolveOriginPolicy({
54
- app: ctx.app,
55
- stack: ctx.stack,
56
- routedAppOrigin: ctx.routedAppOrigin,
57
- extraOrigins: opts.allowedOrigins ?? []
58
- }),
59
+ policy,
59
60
  accountsByAddress
60
61
  });
61
62
  const walletUrl = ctx.routerFrontedUrl ?? `http://${directUrlHostForBindAddress(bindAddress)}:${port}`;
@@ -63,6 +64,7 @@ const acquireWallet = (opts, ctx) => Effect.gen(function* () {
63
64
  const connection = {
64
65
  walletUrl,
65
66
  network: ctx.network,
67
+ allowedOrigins: describeAllowedOrigins(policy),
66
68
  protocolPaths: {
67
69
  health: WalletHttpPath.HEALTH,
68
70
  accounts: WalletHttpPath.ACCOUNTS,
@@ -1 +1 @@
1
- {"version":3,"file":"service.mjs","names":[],"sources":["../../../src/plugins/wallet/service.ts"],"sourcesContent":["// Wallet plugin — main acquire Effect.\n//\n// What this file does (15-wallet.md §Lifecycle, Startup ordered):\n//\n// 1. Resolve each consumed account tag and key the AccountValues by\n// address (load-bearing: sign endpoints look up by address).\n// 2. Allocate a port via the substrate's `PortBrokerService`.\n// 3. Mint or rehydrate the pairing token (read-existing-or-mint at\n// `<stateRoot>/wallet/token`, mode 0o600) via the substrate's\n// `atomicWriteFile`.\n// 4. Resolve the stack-scoped origin allowlist via\n// `origin-policy.ts`.\n// 5. Start the in-process HTTP server.\n// 6. Compose the wallet URL + pair URL + return the resolved\n// `WalletValue`.\n//\n// The substrate handles:\n//\n// - Scope finalizers (port release, server close) — declared by the\n// substrate primitives we call into.\n// - Endpoint registry publication (we surface the URL/pairUrl via\n// our resolved tag value; the substrate's endpoint-registry\n// primitive consumes the `RoutableDecl` from `routable.ts`).\n// - Manifest emit — the substrate projects from registry. We never\n// write `.devstack/manifest.json` directly.\n\nimport { Effect } from 'effect';\nimport type { FileSystem, Scope } from 'effect';\n\nimport type { AccountResourceId, AccountValue } from '../account/index.ts';\nimport type { DevWalletConnection } from './codegen.ts';\nimport { walletBootError, type WalletBootError } from './errors.ts';\nimport { resolveOriginPolicy } from './origin-policy.ts';\nimport { acquirePairingToken, composePairUrl, tokenPath, type PairingToken } from './pairing.ts';\nimport { startHttpServer, type WalletServerConfig, type WalletServerHandle } from './server.ts';\nimport { WalletHttpPath } from './protocol.ts';\n\n// ----------------------------------------------------------------------\n// User-facing options\n// ----------------------------------------------------------------------\n\nimport type { ResourceRef } from '../../api/define-plugin.ts';\n\n/** Literal sentinel for `WalletOptions.accounts: 'all'` — every account\n * member in the stack. Expanded by the composer at `defineDevstack`\n * call time (api-surface-design §4 D6). Kept as an exported constant\n * so the wallet factory + composer share one source of truth. */\nexport const WALLET_ACCOUNTS_ALL = 'all' as const;\nexport type WalletAccountsAll = typeof WALLET_ACCOUNTS_ALL;\n\n/** A user-supplied account ref. The user passes the result of\n * `account('alice')` — NOT a bare string. Generic over the literal\n * account name so the wallet's dependency tuple preserves each\n * per-account resource id (`account/alice`, `account/bob`, ...). */\nexport type WalletAccountMember<Name extends string = string> = ResourceRef<\n\tAccountResourceId<Name>,\n\tAccountValue\n>;\n\nexport interface WalletOptions<\n\tAccounts extends ReadonlyArray<WalletAccountMember> = ReadonlyArray<WalletAccountMember>,\n> {\n\t/** Accounts the wallet binds. Each is yielded for ordering AND its\n\t * resolved value is keyed by address into the sign-handler map.\n\t *\n\t * Two shapes:\n\t *\n\t * - Explicit tuple — each entry is the plugin/resource ref returned\n\t * by `account('name')`. Pins the bound set at the wallet's call\n\t * site; preserves each literal `account/${Name}` so stack\n\t * composition can validate and recursively expand the refs.\n\t *\n\t * - The literal `'all'` — shorthand for \"every account member in\n\t * the stack\". The composer expands this against the final\n\t * member tuple at `defineDevstack(...)` time (api-surface-design\n\t * §4 D6). The wallet member returned by the factory carries an\n\t * expander hook keyed off `WALLET_ACCOUNTS_ALL` that the\n\t * composer invokes once the account-providing members are\n\t * known. */\n\treadonly accounts: Accounts | typeof WALLET_ACCOUNTS_ALL;\n\t/** Extra origins merged on top of the router-fronted dev-server\n\t * origin. Useful for headless test runners and custom dev hosts. */\n\treadonly allowedOrigins?: ReadonlyArray<string>;\n\t/** Preferred host port. Substrate's port broker forward-scans if\n\t * this is taken. Default: substrate-picked (no preference). */\n\treadonly port?: number;\n\t/** NIC the HTTP server binds. Defaults to `'0.0.0.0'` because the\n\t * router runs in Docker and must reach the host process through the\n\t * host-gateway address on native Linux. The public wallet URL remains\n\t * router-fronted and stack-scoped. */\n\treadonly bindAddress?: string;\n}\n\n// ----------------------------------------------------------------------\n// Resolved value (what the wallet plugin publishes)\n// ----------------------------------------------------------------------\n\nexport interface WalletValue {\n\treadonly url: string; // router-fronted URL when available, loopback otherwise\n\treadonly pairUrl: string;\n\treadonly connection: DevWalletConnection;\n\treadonly localPort: number;\n\treadonly token: PairingToken;\n\t/** Server handle — substrate's scope finalizer chain invokes\n\t * `.close()`; callers don't reach in. Exposed here so tests can\n\t * drive teardown explicitly. */\n\treadonly server: WalletServerHandle;\n}\n\nconst WALLET_DEFAULT_BIND_ADDRESS = '0.0.0.0' as const;\nconst WALLET_DIRECT_URL_HOST = '127.0.0.1' as const;\ntype WalletPortProbeHost = '127.0.0.1' | '0.0.0.0';\n\nconst portProbeHostForBindAddress = (bindAddress: string): WalletPortProbeHost =>\n\tbindAddress === WALLET_DEFAULT_BIND_ADDRESS ? '0.0.0.0' : '127.0.0.1';\n\nconst directUrlHostForBindAddress = (bindAddress: string): string =>\n\tbindAddress === WALLET_DEFAULT_BIND_ADDRESS ? WALLET_DIRECT_URL_HOST : bindAddress;\n\n// ----------------------------------------------------------------------\n// Per-acquire context — supplied by the barrel\n// ----------------------------------------------------------------------\n\n/** Inputs from the substrate. The barrel fills these from the\n * BuildContext; tests can construct directly. */\nexport interface WalletAcquireContext {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly network: string;\n\t/** State root where `wallet/token` lives. Convention:\n\t * `<appDir>/.devstack/stacks/<stack>/runtime`. */\n\treadonly stateRoot: string;\n\t/** Port broker seam — returns the allocated port + a scope-\n\t * finalizer-installed release. The barrel adapts the substrate's\n\t * `PortBrokerService.allocate` to this signature so tests can pin\n\t * the port without yielding from a substrate Layer. */\n\treadonly allocatePort: (\n\t\tpreferred?: number,\n\t\tprobeHost?: WalletPortProbeHost,\n\t) => Effect.Effect<number, WalletBootError, Scope.Scope>;\n\t/** Account value resolver — the barrel hands this in keyed off the\n\t * BuildContext so the service body stays substrate-agnostic. */\n\treadonly resolveAccounts: () => Effect.Effect<ReadonlyArray<AccountValue>, WalletBootError>;\n\t/** Stable router-fronted base URL for this wallet on the stack-scoped\n\t * hostname (e.g. `http://wallet.<app>.localhost:<router-port>`). Null\n\t * only in tests that bypass the router derivation. */\n\treadonly routerFrontedUrl: string | null;\n\t/** Stable router-fronted dev-server origin for this stack. Added to\n\t * the wallet allowlist so app+wallet stacks work without repeating\n\t * router origins in user configs. */\n\treadonly routedAppOrigin: string | null;\n}\n\n// ----------------------------------------------------------------------\n// Acquire\n// ----------------------------------------------------------------------\n\n/**\n * Acquire the wallet service.\n *\n * Distilled-doc invariants honored:\n *\n * - C12 (mandatory Origin + bearer): both gates are wired in\n * `server.ts:dispatch`.\n * - Token comparison constant-time: `pairing.ts:safeBearerEquals`.\n * - Token file 0o600: `pairing.ts:acquirePairingToken`.\n * - Token in URL fragment only: `pairing.ts:composePairUrl`.\n * - Token NEVER in log lines: handlers log only `bearerValid:\n * boolean`.\n * - Default bindAddress `'0.0.0.0'`: required for the Docker router to\n * reach host-loopback services on native Linux.\n * - Stack-scoped origin allowlist (no cross-stack pairing risk):\n * `origin-policy.ts:resolveOriginPolicy`.\n */\nexport const acquireWallet = (\n\topts: WalletOptions,\n\tctx: WalletAcquireContext,\n): Effect.Effect<WalletValue, WalletBootError, Scope.Scope | FileSystem.FileSystem> =>\n\tEffect.gen(function* () {\n\t\t// 1. Resolve the dependency account values. The barrel sets up\n\t\t// `resolveAccounts` to walk the BuildContext via resolved dependencies in\n\t\t// `opts.accounts`; we just project to the address-keyed map.\n\t\tconst accounts = yield* ctx.resolveAccounts();\n\t\tif (accounts.length === 0) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalletBootError({\n\t\t\t\t\tphase: 'no-accounts',\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"wallet resolved zero accounts; add account('name') to the stack or pass accounts explicitly.\",\n\t\t\t\t\thint: \"`wallet()` and `wallet({ accounts: 'all' })` require at least one account member in the final stack.\",\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst accountsByAddress = new Map<string, AccountValue>();\n\t\tfor (const acct of accounts) {\n\t\t\t// Two accounts resolving to the same address would silently\n\t\t\t// last-write-wins the sign-route map, so a sign request for\n\t\t\t// that address binds to a non-deterministic account. Fail at\n\t\t\t// boot with the colliding address named.\n\t\t\tif (accountsByAddress.has(acct.address)) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\twalletBootError({\n\t\t\t\t\t\tphase: 'bind-account',\n\t\t\t\t\t\tmessage: `wallet resolved two accounts at the same address ${acct.address}; each account must own a distinct address.`,\n\t\t\t\t\t\thint: 'Remove the duplicate account member, or give the colliding accounts distinct keypairs.',\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t\taccountsByAddress.set(acct.address, acct);\n\t\t}\n\n\t\t// 2. Port allocation. Probe the same host family the real\n\t\t// listener uses, otherwise a process bound on a non-loopback\n\t\t// interface could race the wallet's all-interface listen.\n\t\tconst bindAddress = opts.bindAddress ?? WALLET_DEFAULT_BIND_ADDRESS;\n\t\tconst port = yield* ctx.allocatePort(opts.port, portProbeHostForBindAddress(bindAddress));\n\n\t\t// 3. Token mint or rehydrate. Lives at the stack-scoped state root\n\t\t// so warm-start + snapshot-restore preserve the existing\n\t\t// pairing.\n\t\tconst token = yield* acquirePairingToken(tokenPath(ctx.stateRoot));\n\n\t\t// 4. Origin policy. Allowlist is the router-fronted dev-server\n\t\t// origin for this stack plus any explicit `allowedOrigins`.\n\t\tconst policy = yield* resolveOriginPolicy({\n\t\t\tapp: ctx.app,\n\t\t\tstack: ctx.stack,\n\t\t\troutedAppOrigin: ctx.routedAppOrigin,\n\t\t\textraOrigins: opts.allowedOrigins ?? [],\n\t\t});\n\n\t\t// 5. Start the HTTP server. The dispatcher in `server.ts` owns\n\t\t// route matching + the constant-time bearer compare + the\n\t\t// JSON envelope contract.\n\t\tconst serverConfig: WalletServerConfig = {\n\t\t\tbindAddress,\n\t\t\tport,\n\t\t\ttoken,\n\t\t\tpolicy,\n\t\t\taccountsByAddress,\n\t\t};\n\t\tconst server = yield* startHttpServer(serverConfig);\n\n\t\t// 6. Compose URLs. Router-fronted form when available, loopback\n\t\t// fallback otherwise. The token rides ONLY the fragment — never\n\t\t// a query param — per C13.\n\t\tconst walletUrl =\n\t\t\tctx.routerFrontedUrl ?? `http://${directUrlHostForBindAddress(bindAddress)}:${port}`;\n\t\tconst pairUrl = composePairUrl(walletUrl, token);\n\n\t\t// NON-secret connection metadata only — the token never rides this\n\t\t// (it stays in the `0o600` side-channel file the Vite plugin reads by\n\t\t// path; `pairUrl`/`token` remain on the resolved value for tests and the\n\t\t// snapshot subtree, but are NOT routed through `deployment.json`).\n\t\tconst connection: DevWalletConnection = {\n\t\t\twalletUrl,\n\t\t\tnetwork: ctx.network,\n\t\t\tprotocolPaths: {\n\t\t\t\thealth: WalletHttpPath.HEALTH,\n\t\t\t\taccounts: WalletHttpPath.ACCOUNTS,\n\t\t\t\tsignTransaction: WalletHttpPath.SIGN_TRANSACTION,\n\t\t\t\tsignPersonalMessage: WalletHttpPath.SIGN_PERSONAL_MESSAGE,\n\t\t\t},\n\t\t};\n\n\t\t// Defensive: NEVER log `pairUrl` directly. It carries the token.\n\t\tyield* Effect.logInfo('wallet ready').pipe(\n\t\t\tEffect.annotateLogs({\n\t\t\t\t'wallet.url': walletUrl,\n\t\t\t\t'wallet.token': 'redacted-fragment',\n\t\t\t}),\n\t\t);\n\n\t\treturn {\n\t\t\turl: walletUrl,\n\t\t\tpairUrl,\n\t\t\tconnection,\n\t\t\tlocalPort: port,\n\t\t\ttoken,\n\t\t\tserver,\n\t\t} satisfies WalletValue;\n\t});\n"],"mappings":";;;;;;;;AA6GA,MAAM,8BAA8B;AACpC,MAAM,yBAAyB;AAG/B,MAAM,+BAA+B,gBACpC,gBAAgB,8BAA8B,YAAY;AAE3D,MAAM,+BAA+B,gBACpC,gBAAgB,8BAA8B,yBAAyB;;;;;;;;;;;;;;;;;;AAyDxE,MAAa,iBACZ,MACA,QAEA,OAAO,IAAI,aAAa;CAIvB,MAAM,WAAW,OAAO,IAAI,gBAAgB;CAC5C,IAAI,SAAS,WAAW,GACvB,OAAO,OAAO,OAAO,KACpB,gBAAgB;EACf,OAAO;EACP,SACC;EACD,MAAM;CACP,CAAC,CACF;CAED,MAAM,oCAAoB,IAAI,IAA0B;CACxD,KAAK,MAAM,QAAQ,UAAU;EAK5B,IAAI,kBAAkB,IAAI,KAAK,OAAO,GACrC,OAAO,OAAO,OAAO,KACpB,gBAAgB;GACf,OAAO;GACP,SAAS,oDAAoD,KAAK,QAAQ;GAC1E,MAAM;EACP,CAAC,CACF;EAED,kBAAkB,IAAI,KAAK,SAAS,IAAI;CACzC;CAKA,MAAM,cAAc,KAAK,eAAe;CACxC,MAAM,OAAO,OAAO,IAAI,aAAa,KAAK,MAAM,4BAA4B,WAAW,CAAC;CAKxF,MAAM,QAAQ,OAAO,oBAAoB,UAAU,IAAI,SAAS,CAAC;CAqBjE,MAAM,SAAS,OAAO,gBAAgB;EANrC;EACA;EACA;EACA,QAAA,OAdqB,oBAAoB;GACzC,KAAK,IAAI;GACT,OAAO,IAAI;GACX,iBAAiB,IAAI;GACrB,cAAc,KAAK,kBAAkB,CAAC;EACvC,CAAC;EAUA;CAEgD,CAAC;CAKlD,MAAM,YACL,IAAI,oBAAoB,UAAU,4BAA4B,WAAW,EAAE,GAAG;CAC/E,MAAM,UAAU,eAAe,WAAW,KAAK;CAM/C,MAAM,aAAkC;EACvC;EACA,SAAS,IAAI;EACb,eAAe;GACd,QAAQ,eAAe;GACvB,UAAU,eAAe;GACzB,iBAAiB,eAAe;GAChC,qBAAqB,eAAe;EACrC;CACD;CAGA,OAAO,OAAO,QAAQ,cAAc,CAAC,CAAC,KACrC,OAAO,aAAa;EACnB,cAAc;EACd,gBAAgB;CACjB,CAAC,CACF;CAEA,OAAO;EACN,KAAK;EACL;EACA;EACA,WAAW;EACX;EACA;CACD;AACD,CAAC"}
1
+ {"version":3,"file":"service.mjs","names":[],"sources":["../../../src/plugins/wallet/service.ts"],"sourcesContent":["// Wallet plugin — main acquire Effect.\n//\n// What this file does (15-wallet.md §Lifecycle, Startup ordered):\n//\n// 1. Resolve each consumed account tag and key the AccountValues by\n// address (load-bearing: sign endpoints look up by address).\n// 2. Allocate a port via the substrate's `PortBrokerService`.\n// 3. Mint or rehydrate the pairing token (read-existing-or-mint at\n// `<stateRoot>/wallet/token`, mode 0o600) via the substrate's\n// `atomicWriteFile`.\n// 4. Resolve the stack-scoped origin allowlist via\n// `origin-policy.ts`.\n// 5. Start the in-process HTTP server.\n// 6. Compose the wallet URL + pair URL + return the resolved\n// `WalletValue`.\n//\n// The substrate handles:\n//\n// - Scope finalizers (port release, server close) — declared by the\n// substrate primitives we call into.\n// - Endpoint registry publication (we surface the URL/pairUrl via\n// our resolved tag value; the substrate's endpoint-registry\n// primitive consumes the `RoutableDecl` from `routable.ts`).\n// - Manifest emit — the substrate projects from registry. We never\n// write `.devstack/manifest.json` directly.\n\nimport { Effect } from 'effect';\nimport type { FileSystem, Scope } from 'effect';\n\nimport type { AccountResourceId, AccountValue } from '../account/index.ts';\nimport type { DevWalletConnection } from './codegen.ts';\nimport { walletBootError, type WalletBootError } from './errors.ts';\nimport { describeAllowedOrigins, resolveOriginPolicy } from './origin-policy.ts';\nimport { acquirePairingToken, composePairUrl, tokenPath, type PairingToken } from './pairing.ts';\nimport { startHttpServer, type WalletServerConfig, type WalletServerHandle } from './server.ts';\nimport { WalletHttpPath } from './protocol.ts';\n\n// ----------------------------------------------------------------------\n// User-facing options\n// ----------------------------------------------------------------------\n\nimport type { ResourceRef } from '../../api/define-plugin.ts';\n\n/** Literal sentinel for `WalletOptions.accounts: 'all'` — every account\n * member in the stack. Expanded by the composer at `defineDevstack`\n * call time (api-surface-design §4 D6). Kept as an exported constant\n * so the wallet factory + composer share one source of truth. */\nexport const WALLET_ACCOUNTS_ALL = 'all' as const;\nexport type WalletAccountsAll = typeof WALLET_ACCOUNTS_ALL;\n\n/** A user-supplied account ref. The user passes the result of\n * `account('alice')` — NOT a bare string. Generic over the literal\n * account name so the wallet's dependency tuple preserves each\n * per-account resource id (`account/alice`, `account/bob`, ...). */\nexport type WalletAccountMember<Name extends string = string> = ResourceRef<\n\tAccountResourceId<Name>,\n\tAccountValue\n>;\n\nexport interface WalletOptions<\n\tAccounts extends ReadonlyArray<WalletAccountMember> = ReadonlyArray<WalletAccountMember>,\n> {\n\t/** Accounts the wallet binds. Each is yielded for ordering AND its\n\t * resolved value is keyed by address into the sign-handler map.\n\t *\n\t * Two shapes:\n\t *\n\t * - Explicit tuple — each entry is the plugin/resource ref returned\n\t * by `account('name')`. Pins the bound set at the wallet's call\n\t * site; preserves each literal `account/${Name}` so stack\n\t * composition can validate and recursively expand the refs.\n\t *\n\t * - The literal `'all'` — shorthand for \"every account member in\n\t * the stack\". The composer expands this against the final\n\t * member tuple at `defineDevstack(...)` time (api-surface-design\n\t * §4 D6). The wallet member returned by the factory carries an\n\t * expander hook keyed off `WALLET_ACCOUNTS_ALL` that the\n\t * composer invokes once the account-providing members are\n\t * known. */\n\treadonly accounts: Accounts | typeof WALLET_ACCOUNTS_ALL;\n\t/** Extra origins merged on top of the router-fronted dev-server\n\t * origin. Useful for headless test runners and custom dev hosts. */\n\treadonly allowedOrigins?: ReadonlyArray<string>;\n\t/** Preferred host port. Substrate's port broker forward-scans if\n\t * this is taken. Default: substrate-picked (no preference). */\n\treadonly port?: number;\n\t/** NIC the HTTP server binds. Defaults to `'0.0.0.0'` because the\n\t * router runs in Docker and must reach the host process through the\n\t * host-gateway address on native Linux. The public wallet URL remains\n\t * router-fronted and stack-scoped. */\n\treadonly bindAddress?: string;\n}\n\n// ----------------------------------------------------------------------\n// Resolved value (what the wallet plugin publishes)\n// ----------------------------------------------------------------------\n\nexport interface WalletValue {\n\treadonly url: string; // router-fronted URL when available, loopback otherwise\n\treadonly pairUrl: string;\n\treadonly connection: DevWalletConnection;\n\treadonly localPort: number;\n\treadonly token: PairingToken;\n\t/** Server handle — substrate's scope finalizer chain invokes\n\t * `.close()`; callers don't reach in. Exposed here so tests can\n\t * drive teardown explicitly. */\n\treadonly server: WalletServerHandle;\n}\n\nconst WALLET_DEFAULT_BIND_ADDRESS = '0.0.0.0' as const;\nconst WALLET_DIRECT_URL_HOST = '127.0.0.1' as const;\ntype WalletPortProbeHost = '127.0.0.1' | '0.0.0.0';\n\nconst portProbeHostForBindAddress = (bindAddress: string): WalletPortProbeHost =>\n\tbindAddress === WALLET_DEFAULT_BIND_ADDRESS ? '0.0.0.0' : '127.0.0.1';\n\nconst directUrlHostForBindAddress = (bindAddress: string): string =>\n\tbindAddress === WALLET_DEFAULT_BIND_ADDRESS ? WALLET_DIRECT_URL_HOST : bindAddress;\n\n// ----------------------------------------------------------------------\n// Per-acquire context — supplied by the barrel\n// ----------------------------------------------------------------------\n\n/** Inputs from the substrate. The barrel fills these from the\n * BuildContext; tests can construct directly. */\nexport interface WalletAcquireContext {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly network: string;\n\t/** State root where `wallet/token` lives. Convention:\n\t * `<appDir>/.devstack/stacks/<stack>/runtime`. */\n\treadonly stateRoot: string;\n\t/** Port broker seam — returns the allocated port + a scope-\n\t * finalizer-installed release. The barrel adapts the substrate's\n\t * `PortBrokerService.allocate` to this signature so tests can pin\n\t * the port without yielding from a substrate Layer. */\n\treadonly allocatePort: (\n\t\tpreferred?: number,\n\t\tprobeHost?: WalletPortProbeHost,\n\t) => Effect.Effect<number, WalletBootError, Scope.Scope>;\n\t/** Account value resolver — the barrel hands this in keyed off the\n\t * BuildContext so the service body stays substrate-agnostic. */\n\treadonly resolveAccounts: () => Effect.Effect<ReadonlyArray<AccountValue>, WalletBootError>;\n\t/** Stable router-fronted base URL for this wallet on the stack-scoped\n\t * hostname (e.g. `http://wallet.<app>.localhost:<router-port>`). Null\n\t * only in tests that bypass the router derivation. */\n\treadonly routerFrontedUrl: string | null;\n\t/** Stable router-fronted dev-server origin for this stack. Added to\n\t * the wallet allowlist so app+wallet stacks work without repeating\n\t * router origins in user configs. */\n\treadonly routedAppOrigin: string | null;\n}\n\n// ----------------------------------------------------------------------\n// Acquire\n// ----------------------------------------------------------------------\n\n/**\n * Acquire the wallet service.\n *\n * Distilled-doc invariants honored:\n *\n * - C12 (mandatory Origin + bearer): both gates are wired in\n * `server.ts:dispatch`.\n * - Token comparison constant-time: `pairing.ts:safeBearerEquals`.\n * - Token file 0o600: `pairing.ts:acquirePairingToken`.\n * - Token in URL fragment only: `pairing.ts:composePairUrl`.\n * - Token NEVER in log lines: handlers log only `bearerValid:\n * boolean`.\n * - Default bindAddress `'0.0.0.0'`: required for the Docker router to\n * reach host-loopback services on native Linux.\n * - Stack-scoped origin allowlist (no cross-stack pairing risk):\n * `origin-policy.ts:resolveOriginPolicy`.\n */\nexport const acquireWallet = (\n\topts: WalletOptions,\n\tctx: WalletAcquireContext,\n): Effect.Effect<WalletValue, WalletBootError, Scope.Scope | FileSystem.FileSystem> =>\n\tEffect.gen(function* () {\n\t\t// 1. Resolve the dependency account values. The barrel sets up\n\t\t// `resolveAccounts` to walk the BuildContext via resolved dependencies in\n\t\t// `opts.accounts`; we just project to the address-keyed map.\n\t\tconst accounts = yield* ctx.resolveAccounts();\n\t\tif (accounts.length === 0) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalletBootError({\n\t\t\t\t\tphase: 'no-accounts',\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"wallet resolved zero accounts; add account('name') to the stack or pass accounts explicitly.\",\n\t\t\t\t\thint: \"`wallet()` and `wallet({ accounts: 'all' })` require at least one account member in the final stack.\",\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst accountsByAddress = new Map<string, AccountValue>();\n\t\tfor (const acct of accounts) {\n\t\t\t// Two accounts resolving to the same address would silently\n\t\t\t// last-write-wins the sign-route map, so a sign request for\n\t\t\t// that address binds to a non-deterministic account. Fail at\n\t\t\t// boot with the colliding address named.\n\t\t\tif (accountsByAddress.has(acct.address)) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\twalletBootError({\n\t\t\t\t\t\tphase: 'bind-account',\n\t\t\t\t\t\tmessage: `wallet resolved two accounts at the same address ${acct.address}; each account must own a distinct address.`,\n\t\t\t\t\t\thint: 'Remove the duplicate account member, or give the colliding accounts distinct keypairs.',\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t\taccountsByAddress.set(acct.address, acct);\n\t\t}\n\n\t\t// 2. Port allocation. Probe the same host family the real\n\t\t// listener uses, otherwise a process bound on a non-loopback\n\t\t// interface could race the wallet's all-interface listen.\n\t\tconst bindAddress = opts.bindAddress ?? WALLET_DEFAULT_BIND_ADDRESS;\n\t\tconst port = yield* ctx.allocatePort(opts.port, portProbeHostForBindAddress(bindAddress));\n\n\t\t// 3. Token mint or rehydrate. Lives at the stack-scoped state root\n\t\t// so warm-start + snapshot-restore preserve the existing\n\t\t// pairing.\n\t\tconst token = yield* acquirePairingToken(tokenPath(ctx.stateRoot));\n\n\t\t// 4. Origin policy. Allowlist is the router-fronted dev-server\n\t\t// origin for this stack plus any explicit `allowedOrigins`.\n\t\tconst policy = yield* resolveOriginPolicy({\n\t\t\tapp: ctx.app,\n\t\t\tstack: ctx.stack,\n\t\t\troutedAppOrigin: ctx.routedAppOrigin,\n\t\t\textraOrigins: opts.allowedOrigins ?? [],\n\t\t});\n\n\t\t// 5. Start the HTTP server. The dispatcher in `server.ts` owns\n\t\t// route matching + the constant-time bearer compare + the\n\t\t// JSON envelope contract.\n\t\tconst serverConfig: WalletServerConfig = {\n\t\t\tbindAddress,\n\t\t\tport,\n\t\t\ttoken,\n\t\t\tpolicy,\n\t\t\taccountsByAddress,\n\t\t};\n\t\tconst server = yield* startHttpServer(serverConfig);\n\n\t\t// 6. Compose URLs. Router-fronted form when available, loopback\n\t\t// fallback otherwise. The token rides ONLY the fragment — never\n\t\t// a query param — per C13.\n\t\tconst walletUrl =\n\t\t\tctx.routerFrontedUrl ?? `http://${directUrlHostForBindAddress(bindAddress)}:${port}`;\n\t\tconst pairUrl = composePairUrl(walletUrl, token);\n\n\t\t// NON-secret connection metadata only — the token never rides this\n\t\t// (it stays in the `0o600` side-channel file the Vite plugin reads by\n\t\t// path; `pairUrl`/`token` remain on the resolved value for tests and the\n\t\t// snapshot subtree, but are NOT routed through `deployment.json`).\n\t\tconst connection: DevWalletConnection = {\n\t\t\twalletUrl,\n\t\t\tnetwork: ctx.network,\n\t\t\tallowedOrigins: describeAllowedOrigins(policy),\n\t\t\tprotocolPaths: {\n\t\t\t\thealth: WalletHttpPath.HEALTH,\n\t\t\t\taccounts: WalletHttpPath.ACCOUNTS,\n\t\t\t\tsignTransaction: WalletHttpPath.SIGN_TRANSACTION,\n\t\t\t\tsignPersonalMessage: WalletHttpPath.SIGN_PERSONAL_MESSAGE,\n\t\t\t},\n\t\t};\n\n\t\t// Defensive: NEVER log `pairUrl` directly. It carries the token.\n\t\tyield* Effect.logInfo('wallet ready').pipe(\n\t\t\tEffect.annotateLogs({\n\t\t\t\t'wallet.url': walletUrl,\n\t\t\t\t'wallet.token': 'redacted-fragment',\n\t\t\t}),\n\t\t);\n\n\t\treturn {\n\t\t\turl: walletUrl,\n\t\t\tpairUrl,\n\t\t\tconnection,\n\t\t\tlocalPort: port,\n\t\t\ttoken,\n\t\t\tserver,\n\t\t} satisfies WalletValue;\n\t});\n"],"mappings":";;;;;;;;AA6GA,MAAM,8BAA8B;AACpC,MAAM,yBAAyB;AAG/B,MAAM,+BAA+B,gBACpC,gBAAgB,8BAA8B,YAAY;AAE3D,MAAM,+BAA+B,gBACpC,gBAAgB,8BAA8B,yBAAyB;;;;;;;;;;;;;;;;;;AAyDxE,MAAa,iBACZ,MACA,QAEA,OAAO,IAAI,aAAa;CAIvB,MAAM,WAAW,OAAO,IAAI,gBAAgB;CAC5C,IAAI,SAAS,WAAW,GACvB,OAAO,OAAO,OAAO,KACpB,gBAAgB;EACf,OAAO;EACP,SACC;EACD,MAAM;CACP,CAAC,CACF;CAED,MAAM,oCAAoB,IAAI,IAA0B;CACxD,KAAK,MAAM,QAAQ,UAAU;EAK5B,IAAI,kBAAkB,IAAI,KAAK,OAAO,GACrC,OAAO,OAAO,OAAO,KACpB,gBAAgB;GACf,OAAO;GACP,SAAS,oDAAoD,KAAK,QAAQ;GAC1E,MAAM;EACP,CAAC,CACF;EAED,kBAAkB,IAAI,KAAK,SAAS,IAAI;CACzC;CAKA,MAAM,cAAc,KAAK,eAAe;CACxC,MAAM,OAAO,OAAO,IAAI,aAAa,KAAK,MAAM,4BAA4B,WAAW,CAAC;CAKxF,MAAM,QAAQ,OAAO,oBAAoB,UAAU,IAAI,SAAS,CAAC;CAIjE,MAAM,SAAS,OAAO,oBAAoB;EACzC,KAAK,IAAI;EACT,OAAO,IAAI;EACX,iBAAiB,IAAI;EACrB,cAAc,KAAK,kBAAkB,CAAC;CACvC,CAAC;CAYD,MAAM,SAAS,OAAO,gBAAgB;EANrC;EACA;EACA;EACA;EACA;CAEgD,CAAC;CAKlD,MAAM,YACL,IAAI,oBAAoB,UAAU,4BAA4B,WAAW,EAAE,GAAG;CAC/E,MAAM,UAAU,eAAe,WAAW,KAAK;CAM/C,MAAM,aAAkC;EACvC;EACA,SAAS,IAAI;EACb,gBAAgB,uBAAuB,MAAM;EAC7C,eAAe;GACd,QAAQ,eAAe;GACvB,UAAU,eAAe;GACzB,iBAAiB,eAAe;GAChC,qBAAqB,eAAe;EACrC;CACD;CAGA,OAAO,OAAO,QAAQ,cAAc,CAAC,CAAC,KACrC,OAAO,aAAa;EACnB,cAAc;EACd,gBAAgB;CACjB,CAAC,CACF;CAEA,OAAO;EACN,KAAK;EACL;EACA;EACA,WAAW;EACX;EACA;CACD;AACD,CAAC"}
@@ -7,6 +7,7 @@ import { walrusPluginError } from "./errors.mjs";
7
7
  import { Effect, Scope } from "effect";
8
8
  //#region src/plugins/walrus/client-services.ts
9
9
  const DEFAULT_WALRUS_CLIENT_SERVICE_PORT = 31415;
10
+ const DEFAULT_WALRUS_UPLOAD_RELAY_SERVICE_PORT = 3e3;
10
11
  const WALRUS_CLIENT_SERVICE_STOP_SIGNAL = "SIGINT";
11
12
  const WALRUS_CLIENT_CONFIG_FILE = "client_config.yaml";
12
13
  const WALRUS_CLIENT_WALLET_FILE = "sui_client.yaml";
@@ -23,6 +24,7 @@ const walrusClientServiceConfigHash = (parts) => [
23
24
  `rpc=${parts.suiRpcUrlInNetwork}`
24
25
  ].join("|");
25
26
  const SERVICE_READY_PROBE_INTERVAL_MS = 500;
27
+ const walrusClientServiceReadyPath = (role) => role === "upload-relay" ? "/v1/tip-config" : "/status";
26
28
  const startWalrusClientServices = (runtime, spec) => Effect.gen(function* () {
27
29
  const outerScope = yield* Effect.scope;
28
30
  const serviceStopScope = yield* Scope.fork(outerScope, "parallel");
@@ -84,6 +86,7 @@ const startWalrusClientServices = (runtime, spec) => Effect.gen(function* () {
84
86
  });
85
87
  const handle = yield* Scope.provide(ensureService, serviceStopScope);
86
88
  yield* setCurrentPluginPhase(`waiting for Walrus ${role} HTTP status on 127.0.0.1:${options.port}`);
89
+ const readyPath = walrusClientServiceReadyPath(role);
87
90
  yield* waitForProbe({
88
91
  label: `walrus.${role}`,
89
92
  timeoutMs: readyTimeout,
@@ -91,10 +94,10 @@ const startWalrusClientServices = (runtime, spec) => Effect.gen(function* () {
91
94
  probe: () => runtime.exec(handle, [
92
95
  "sh",
93
96
  "-c",
94
- `curl -fsS --max-time 2 http://127.0.0.1:${options.port}/status >/dev/null`
97
+ `curl -fsS --max-time 2 http://127.0.0.1:${options.port}${readyPath} >/dev/null`
95
98
  ]).pipe(Effect.map(exitCodeProbeResult))
96
99
  }).pipe(Effect.mapError((cause) => {
97
- if (cause instanceof ProbeTimeoutError) return walrusPluginError(role, `walrus ${role} never became ready within ${readyTimeout}ms (container=${containerName}, probe=http://127.0.0.1:${options.port}/status).`, { cause: cause.lastError ?? cause.lastNotReady ?? cause });
100
+ if (cause instanceof ProbeTimeoutError) return walrusPluginError(role, `walrus ${role} never became ready within ${readyTimeout}ms (container=${containerName}, probe=http://127.0.0.1:${options.port}${readyPath}).`, { cause: cause.lastError ?? cause.lastNotReady ?? cause });
98
101
  return walrusPluginError(role, `walrus ${role} ready-probe exec failed: ${cause.reason}: ${cause.detail}`, { cause });
99
102
  }));
100
103
  return {
@@ -103,13 +106,18 @@ const startWalrusClientServices = (runtime, spec) => Effect.gen(function* () {
103
106
  containerPort: options.port
104
107
  };
105
108
  });
106
- const [aggregator, publisher] = yield* Effect.all([spec.options.aggregator === null || spec.images.aggregator === null ? Effect.succeed(null) : startOne("aggregator", spec.images.aggregator, spec.options.aggregator), spec.options.publisher === null || spec.images.publisher === null ? Effect.succeed(null) : startOne("publisher", spec.images.publisher, spec.options.publisher)], { concurrency: "unbounded" });
109
+ const [aggregator, publisher, uploadRelay] = yield* Effect.all([
110
+ spec.options.aggregator === null || spec.images.aggregator === null ? Effect.succeed(null) : startOne("aggregator", spec.images.aggregator, spec.options.aggregator),
111
+ spec.options.publisher === null || spec.images.publisher === null ? Effect.succeed(null) : startOne("publisher", spec.images.publisher, spec.options.publisher),
112
+ spec.options.uploadRelay === null || spec.images.uploadRelay === null ? Effect.succeed(null) : startOne("upload-relay", spec.images.uploadRelay, spec.options.uploadRelay)
113
+ ], { concurrency: "unbounded" });
107
114
  return {
108
115
  aggregator,
109
- publisher
116
+ publisher,
117
+ uploadRelay
110
118
  };
111
119
  });
112
120
  //#endregion
113
- export { DEFAULT_WALRUS_CLIENT_SERVICE_PORT, startWalrusClientServices };
121
+ export { DEFAULT_WALRUS_CLIENT_SERVICE_PORT, DEFAULT_WALRUS_UPLOAD_RELAY_SERVICE_PORT, startWalrusClientServices };
114
122
 
115
123
  //# sourceMappingURL=client-services.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-services.mjs","names":[],"sources":["../../../src/plugins/walrus/client-services.ts"],"sourcesContent":["// Walrus local client-service lifecycle.\n//\n// These are the release-provided `walrus aggregator` and `walrus publisher`\n// subcommands, wrapped only enough to feed them the local deploy outputs and\n// make them routable through devstack.\n\nimport { Effect, Scope } from 'effect';\n\nimport type {\n\tContainerHandle,\n\tContainerRuntime,\n\tImageRef,\n} from '../../contracts/container-runtime.ts';\nimport { HOST_GATEWAY_EXTRA_HOSTS } from '../../substrate/runtime/host-gateway.ts';\nimport { ensureManagedContainer } from '../../substrate/runtime/managed-container.ts';\nimport {\n\tProbeTimeoutError,\n\texitCodeProbeResult,\n\twaitForProbe,\n} from '../../substrate/runtime/probes.ts';\nimport { setCurrentPluginPhase } from '../../substrate/runtime/current-plugin.ts';\nimport { walrusDeployMountPaths } from './deploy-paths.ts';\nimport { walrusPluginError, type WalrusPluginError } from './errors.ts';\n\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_PORT = 31_415;\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_READY_TIMEOUT_MS = 60_000;\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_STOP_GRACE_SECONDS = 10;\nexport const WALRUS_CLIENT_SERVICE_STOP_SIGNAL = 'SIGINT';\nexport const WALRUS_CLIENT_CONFIG_FILE = 'client_config.yaml';\nexport const WALRUS_CLIENT_WALLET_FILE = 'sui_client.yaml';\nexport const WALRUS_CLIENT_KEYSTORE_FILE = 'sui_client.keystore';\n\nexport type WalrusClientServiceRole = 'aggregator' | 'publisher';\n\nexport interface WalrusClientServiceOptions {\n\treadonly port: number;\n}\n\nexport interface WalrusClientService {\n\treadonly role: WalrusClientServiceRole;\n\treadonly containerName: string;\n\treadonly containerPort: number;\n}\n\nexport interface WalrusClientServices {\n\treadonly aggregator: WalrusClientService | null;\n\treadonly publisher: WalrusClientService | null;\n}\n\nexport interface StartWalrusClientServicesSpec {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly images: {\n\t\treadonly aggregator: ImageRef | null;\n\t\treadonly publisher: ImageRef | null;\n\t};\n\treadonly options: {\n\t\treadonly aggregator: WalrusClientServiceOptions | null;\n\t\treadonly publisher: WalrusClientServiceOptions | null;\n\t};\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly deployHostMountPath: string;\n\treadonly stackRoot: string;\n\treadonly deployConfigHash: string;\n\treadonly suiRpcUrlInNetwork: string;\n\treadonly stopGraceSeconds?: number;\n\treadonly readyTimeoutMs?: number;\n}\n\nexport const walrusClientServiceContainerName = (parts: {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly role: WalrusClientServiceRole;\n}): string => `devstack-${parts.app}-${parts.stack}-walrus-${parts.walrusName}-${parts.role}`;\n\nexport const walrusClientServiceConfigHash = (parts: {\n\treadonly role: WalrusClientServiceRole;\n\treadonly deployConfigHash: string;\n\treadonly deploySourceHostPath: string;\n\treadonly deployMountTarget: string;\n\treadonly containerPort: number;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly suiRpcUrlInNetwork: string;\n}): string =>\n\t[\n\t\t'walrus-client-service-v2',\n\t\t`role=${parts.role}`,\n\t\tparts.deployConfigHash,\n\t\t`client=${WALRUS_CLIENT_CONFIG_FILE},${WALRUS_CLIENT_WALLET_FILE},${WALRUS_CLIENT_KEYSTORE_FILE}`,\n\t\t`mount=${parts.deploySourceHostPath}->${parts.deployMountTarget}`,\n\t\t`port=${parts.containerPort}`,\n\t\t`net=${parts.walrusNetworkName},${parts.suiNetworkName}`,\n\t\t`rpc=${parts.suiRpcUrlInNetwork}`,\n\t].join('|');\n\nconst SERVICE_READY_PROBE_INTERVAL_MS = 500;\n\nexport const startWalrusClientServices = (\n\truntime: ContainerRuntime,\n\tspec: StartWalrusClientServicesSpec,\n): Effect.Effect<WalrusClientServices, WalrusPluginError, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tconst outerScope = yield* Effect.scope;\n\t\tconst serviceStopScope = yield* Scope.fork(outerScope, 'parallel');\n\t\tconst deployMount = walrusDeployMountPaths({\n\t\t\tstackRoot: spec.stackRoot,\n\t\t\tdeployOutputDirHostPath: spec.deployHostMountPath,\n\t\t\tmountTarget: '/opt/walrus/runtime',\n\t\t});\n\t\tconst readyTimeout = spec.readyTimeoutMs ?? DEFAULT_WALRUS_CLIENT_SERVICE_READY_TIMEOUT_MS;\n\t\tconst stopGraceSeconds =\n\t\t\tspec.stopGraceSeconds ?? DEFAULT_WALRUS_CLIENT_SERVICE_STOP_GRACE_SECONDS;\n\n\t\tconst startOne = (\n\t\t\trole: WalrusClientServiceRole,\n\t\t\timage: ImageRef,\n\t\t\toptions: WalrusClientServiceOptions,\n\t\t): Effect.Effect<WalrusClientService, WalrusPluginError, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst containerName = walrusClientServiceContainerName({\n\t\t\t\t\tapp: spec.app,\n\t\t\t\t\tstack: spec.stack,\n\t\t\t\t\twalrusName: spec.walrusName,\n\t\t\t\t\trole,\n\t\t\t\t});\n\n\t\t\t\tyield* setCurrentPluginPhase(`creating Walrus ${role} container ${containerName}`);\n\t\t\t\tconst ensureService: Effect.Effect<ContainerHandle, WalrusPluginError, Scope.Scope> =\n\t\t\t\t\tensureManagedContainer<WalrusPluginError>({\n\t\t\t\t\t\truntime,\n\t\t\t\t\t\tidentity: { app: spec.app, stack: spec.stack },\n\t\t\t\t\t\tplugin: 'walrus',\n\t\t\t\t\t\trole,\n\t\t\t\t\t\tspec: {\n\t\t\t\t\t\t\tname: containerName,\n\t\t\t\t\t\t\timage,\n\t\t\t\t\t\t\trecreate: 'on-config-change',\n\t\t\t\t\t\t\tconfigHash: walrusClientServiceConfigHash({\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\tdeployConfigHash: spec.deployConfigHash,\n\t\t\t\t\t\t\t\tdeploySourceHostPath: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\tdeployMountTarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\tcontainerPort: options.port,\n\t\t\t\t\t\t\t\twalrusNetworkName: spec.walrusNetworkName,\n\t\t\t\t\t\t\t\tsuiNetworkName: spec.suiNetworkName,\n\t\t\t\t\t\t\t\tsuiRpcUrlInNetwork: spec.suiRpcUrlInNetwork,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tenv: {\n\t\t\t\t\t\t\t\tDEPLOY_OUTPUT_DIR: deployMount.outputDirInContainer,\n\t\t\t\t\t\t\t\tSUI_RPC_URL: spec.suiRpcUrlInNetwork,\n\t\t\t\t\t\t\t\tWALRUS_CLIENT_SERVICE_BIND_ADDRESS: `0.0.0.0:${options.port}`,\n\t\t\t\t\t\t\t\tWALRUS_CONTAINER_PORT: String(options.port),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcommand: [role],\n\t\t\t\t\t\t\tnetworkAttach: [spec.walrusNetworkName, spec.suiNetworkName],\n\t\t\t\t\t\t\textraHosts: HOST_GATEWAY_EXTRA_HOSTS,\n\t\t\t\t\t\t\tmounts: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tsource: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\t\ttarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\t\treadonly: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tstopGraceSeconds,\n\t\t\t\t\t\t\tstopSignal: WALRUS_CLIENT_SERVICE_STOP_SIGNAL,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmapError: (cause) =>\n\t\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\t`walrus ${role} ensureContainer failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t});\n\t\t\t\tconst handle = yield* Scope.provide(ensureService, serviceStopScope);\n\n\t\t\t\tyield* setCurrentPluginPhase(\n\t\t\t\t\t`waiting for Walrus ${role} HTTP status on 127.0.0.1:${options.port}`,\n\t\t\t\t);\n\t\t\t\tyield* waitForProbe({\n\t\t\t\t\tlabel: `walrus.${role}`,\n\t\t\t\t\ttimeoutMs: readyTimeout,\n\t\t\t\t\tintervalMs: SERVICE_READY_PROBE_INTERVAL_MS,\n\t\t\t\t\tprobe: () =>\n\t\t\t\t\t\truntime\n\t\t\t\t\t\t\t.exec(handle, [\n\t\t\t\t\t\t\t\t'sh',\n\t\t\t\t\t\t\t\t'-c',\n\t\t\t\t\t\t\t\t`curl -fsS --max-time 2 http://127.0.0.1:${options.port}/status >/dev/null`,\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.pipe(Effect.map(exitCodeProbeResult)),\n\t\t\t\t}).pipe(\n\t\t\t\t\tEffect.mapError((cause) => {\n\t\t\t\t\t\tif (cause instanceof ProbeTimeoutError) {\n\t\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\t`walrus ${role} never became ready within ${readyTimeout}ms ` +\n\t\t\t\t\t\t\t\t\t`(container=${containerName}, probe=http://127.0.0.1:${options.port}/status).`,\n\t\t\t\t\t\t\t\t{ cause: cause.lastError ?? cause.lastNotReady ?? cause },\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t`walrus ${role} ready-probe exec failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t);\n\n\t\t\t\treturn { role, containerName, containerPort: options.port };\n\t\t\t});\n\n\t\tconst [aggregator, publisher] = yield* Effect.all(\n\t\t\t[\n\t\t\t\tspec.options.aggregator === null || spec.images.aggregator === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('aggregator', spec.images.aggregator, spec.options.aggregator),\n\t\t\t\tspec.options.publisher === null || spec.images.publisher === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('publisher', spec.images.publisher, spec.options.publisher),\n\t\t\t],\n\t\t\t{ concurrency: 'unbounded' },\n\t\t);\n\n\t\treturn { aggregator, publisher };\n\t});\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-services.mjs","names":[],"sources":["../../../src/plugins/walrus/client-services.ts"],"sourcesContent":["// Walrus local client-service lifecycle.\n//\n// These are the release-provided `walrus aggregator` / `walrus publisher`\n// subcommands plus the standalone `walrus-upload-relay` binary, wrapped only\n// enough to feed them the local deploy outputs and make them routable through\n// devstack.\n\nimport { Effect, Scope } from 'effect';\n\nimport type {\n\tContainerHandle,\n\tContainerRuntime,\n\tImageRef,\n} from '../../contracts/container-runtime.ts';\nimport { HOST_GATEWAY_EXTRA_HOSTS } from '../../substrate/runtime/host-gateway.ts';\nimport { ensureManagedContainer } from '../../substrate/runtime/managed-container.ts';\nimport {\n\tProbeTimeoutError,\n\texitCodeProbeResult,\n\twaitForProbe,\n} from '../../substrate/runtime/probes.ts';\nimport { setCurrentPluginPhase } from '../../substrate/runtime/current-plugin.ts';\nimport { walrusDeployMountPaths } from './deploy-paths.ts';\nimport { walrusPluginError, type WalrusPluginError } from './errors.ts';\n\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_PORT = 31_415;\nexport const DEFAULT_WALRUS_UPLOAD_RELAY_SERVICE_PORT = 3_000;\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_READY_TIMEOUT_MS = 60_000;\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_STOP_GRACE_SECONDS = 10;\nexport const WALRUS_CLIENT_SERVICE_STOP_SIGNAL = 'SIGINT';\nexport const WALRUS_CLIENT_CONFIG_FILE = 'client_config.yaml';\nexport const WALRUS_CLIENT_WALLET_FILE = 'sui_client.yaml';\nexport const WALRUS_CLIENT_KEYSTORE_FILE = 'sui_client.keystore';\n\nexport type WalrusClientServiceRole = 'aggregator' | 'publisher' | 'upload-relay';\n\nexport interface WalrusClientServiceOptions {\n\treadonly port: number;\n}\n\nexport interface WalrusClientService {\n\treadonly role: WalrusClientServiceRole;\n\treadonly containerName: string;\n\treadonly containerPort: number;\n}\n\nexport interface WalrusClientServices {\n\treadonly aggregator: WalrusClientService | null;\n\treadonly publisher: WalrusClientService | null;\n\treadonly uploadRelay: WalrusClientService | null;\n}\n\nexport interface StartWalrusClientServicesSpec {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly images: {\n\t\treadonly aggregator: ImageRef | null;\n\t\treadonly publisher: ImageRef | null;\n\t\treadonly uploadRelay: ImageRef | null;\n\t};\n\treadonly options: {\n\t\treadonly aggregator: WalrusClientServiceOptions | null;\n\t\treadonly publisher: WalrusClientServiceOptions | null;\n\t\treadonly uploadRelay: WalrusClientServiceOptions | null;\n\t};\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly deployHostMountPath: string;\n\treadonly stackRoot: string;\n\treadonly deployConfigHash: string;\n\treadonly suiRpcUrlInNetwork: string;\n\treadonly stopGraceSeconds?: number;\n\treadonly readyTimeoutMs?: number;\n}\n\nexport const walrusClientServiceContainerName = (parts: {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly role: WalrusClientServiceRole;\n}): string => `devstack-${parts.app}-${parts.stack}-walrus-${parts.walrusName}-${parts.role}`;\n\nexport const walrusClientServiceConfigHash = (parts: {\n\treadonly role: WalrusClientServiceRole;\n\treadonly deployConfigHash: string;\n\treadonly deploySourceHostPath: string;\n\treadonly deployMountTarget: string;\n\treadonly containerPort: number;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly suiRpcUrlInNetwork: string;\n}): string =>\n\t[\n\t\t'walrus-client-service-v2',\n\t\t`role=${parts.role}`,\n\t\tparts.deployConfigHash,\n\t\t`client=${WALRUS_CLIENT_CONFIG_FILE},${WALRUS_CLIENT_WALLET_FILE},${WALRUS_CLIENT_KEYSTORE_FILE}`,\n\t\t`mount=${parts.deploySourceHostPath}->${parts.deployMountTarget}`,\n\t\t`port=${parts.containerPort}`,\n\t\t`net=${parts.walrusNetworkName},${parts.suiNetworkName}`,\n\t\t`rpc=${parts.suiRpcUrlInNetwork}`,\n\t].join('|');\n\nconst SERVICE_READY_PROBE_INTERVAL_MS = 500;\n\nconst walrusClientServiceReadyPath = (role: WalrusClientServiceRole): string =>\n\trole === 'upload-relay' ? '/v1/tip-config' : '/status';\n\nexport const startWalrusClientServices = (\n\truntime: ContainerRuntime,\n\tspec: StartWalrusClientServicesSpec,\n): Effect.Effect<WalrusClientServices, WalrusPluginError, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tconst outerScope = yield* Effect.scope;\n\t\tconst serviceStopScope = yield* Scope.fork(outerScope, 'parallel');\n\t\tconst deployMount = walrusDeployMountPaths({\n\t\t\tstackRoot: spec.stackRoot,\n\t\t\tdeployOutputDirHostPath: spec.deployHostMountPath,\n\t\t\tmountTarget: '/opt/walrus/runtime',\n\t\t});\n\t\tconst readyTimeout = spec.readyTimeoutMs ?? DEFAULT_WALRUS_CLIENT_SERVICE_READY_TIMEOUT_MS;\n\t\tconst stopGraceSeconds =\n\t\t\tspec.stopGraceSeconds ?? DEFAULT_WALRUS_CLIENT_SERVICE_STOP_GRACE_SECONDS;\n\n\t\tconst startOne = (\n\t\t\trole: WalrusClientServiceRole,\n\t\t\timage: ImageRef,\n\t\t\toptions: WalrusClientServiceOptions,\n\t\t): Effect.Effect<WalrusClientService, WalrusPluginError, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst containerName = walrusClientServiceContainerName({\n\t\t\t\t\tapp: spec.app,\n\t\t\t\t\tstack: spec.stack,\n\t\t\t\t\twalrusName: spec.walrusName,\n\t\t\t\t\trole,\n\t\t\t\t});\n\n\t\t\t\tyield* setCurrentPluginPhase(`creating Walrus ${role} container ${containerName}`);\n\t\t\t\tconst ensureService: Effect.Effect<ContainerHandle, WalrusPluginError, Scope.Scope> =\n\t\t\t\t\tensureManagedContainer<WalrusPluginError>({\n\t\t\t\t\t\truntime,\n\t\t\t\t\t\tidentity: { app: spec.app, stack: spec.stack },\n\t\t\t\t\t\tplugin: 'walrus',\n\t\t\t\t\t\trole,\n\t\t\t\t\t\tspec: {\n\t\t\t\t\t\t\tname: containerName,\n\t\t\t\t\t\t\timage,\n\t\t\t\t\t\t\trecreate: 'on-config-change',\n\t\t\t\t\t\t\tconfigHash: walrusClientServiceConfigHash({\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\tdeployConfigHash: spec.deployConfigHash,\n\t\t\t\t\t\t\t\tdeploySourceHostPath: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\tdeployMountTarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\tcontainerPort: options.port,\n\t\t\t\t\t\t\t\twalrusNetworkName: spec.walrusNetworkName,\n\t\t\t\t\t\t\t\tsuiNetworkName: spec.suiNetworkName,\n\t\t\t\t\t\t\t\tsuiRpcUrlInNetwork: spec.suiRpcUrlInNetwork,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tenv: {\n\t\t\t\t\t\t\t\tDEPLOY_OUTPUT_DIR: deployMount.outputDirInContainer,\n\t\t\t\t\t\t\t\tSUI_RPC_URL: spec.suiRpcUrlInNetwork,\n\t\t\t\t\t\t\t\tWALRUS_CLIENT_SERVICE_BIND_ADDRESS: `0.0.0.0:${options.port}`,\n\t\t\t\t\t\t\t\tWALRUS_CONTAINER_PORT: String(options.port),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcommand: [role],\n\t\t\t\t\t\t\tnetworkAttach: [spec.walrusNetworkName, spec.suiNetworkName],\n\t\t\t\t\t\t\textraHosts: HOST_GATEWAY_EXTRA_HOSTS,\n\t\t\t\t\t\t\tmounts: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tsource: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\t\ttarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\t\treadonly: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tstopGraceSeconds,\n\t\t\t\t\t\t\tstopSignal: WALRUS_CLIENT_SERVICE_STOP_SIGNAL,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmapError: (cause) =>\n\t\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\t`walrus ${role} ensureContainer failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t});\n\t\t\t\tconst handle = yield* Scope.provide(ensureService, serviceStopScope);\n\n\t\t\t\tyield* setCurrentPluginPhase(\n\t\t\t\t\t`waiting for Walrus ${role} HTTP status on 127.0.0.1:${options.port}`,\n\t\t\t\t);\n\t\t\t\tconst readyPath = walrusClientServiceReadyPath(role);\n\t\t\t\tyield* waitForProbe({\n\t\t\t\t\tlabel: `walrus.${role}`,\n\t\t\t\t\ttimeoutMs: readyTimeout,\n\t\t\t\t\tintervalMs: SERVICE_READY_PROBE_INTERVAL_MS,\n\t\t\t\t\tprobe: () =>\n\t\t\t\t\t\truntime\n\t\t\t\t\t\t\t.exec(handle, [\n\t\t\t\t\t\t\t\t'sh',\n\t\t\t\t\t\t\t\t'-c',\n\t\t\t\t\t\t\t\t`curl -fsS --max-time 2 http://127.0.0.1:${options.port}${readyPath} >/dev/null`,\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.pipe(Effect.map(exitCodeProbeResult)),\n\t\t\t\t}).pipe(\n\t\t\t\t\tEffect.mapError((cause) => {\n\t\t\t\t\t\tif (cause instanceof ProbeTimeoutError) {\n\t\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\t`walrus ${role} never became ready within ${readyTimeout}ms ` +\n\t\t\t\t\t\t\t\t\t`(container=${containerName}, probe=http://127.0.0.1:${options.port}${readyPath}).`,\n\t\t\t\t\t\t\t\t{ cause: cause.lastError ?? cause.lastNotReady ?? cause },\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t`walrus ${role} ready-probe exec failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t);\n\n\t\t\t\treturn { role, containerName, containerPort: options.port };\n\t\t\t});\n\n\t\tconst [aggregator, publisher, uploadRelay] = yield* Effect.all(\n\t\t\t[\n\t\t\t\tspec.options.aggregator === null || spec.images.aggregator === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('aggregator', spec.images.aggregator, spec.options.aggregator),\n\t\t\t\tspec.options.publisher === null || spec.images.publisher === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('publisher', spec.images.publisher, spec.options.publisher),\n\t\t\t\tspec.options.uploadRelay === null || spec.images.uploadRelay === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('upload-relay', spec.images.uploadRelay, spec.options.uploadRelay),\n\t\t\t],\n\t\t\t{ concurrency: 'unbounded' },\n\t\t);\n\n\t\treturn { aggregator, publisher, uploadRelay };\n\t});\n"],"mappings":"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"}
@@ -20,6 +20,7 @@ interface WalrusBindings {
20
20
  readonly proxyUrl: string | null;
21
21
  readonly aggregatorUrl: string | null;
22
22
  readonly publisherUrl: string | null;
23
+ readonly uploadRelayUrl: string | null;
23
24
  readonly nodes: ReadonlyArray<WalrusNodeBinding>;
24
25
  }
25
26
  //#endregion
@@ -75,6 +75,7 @@ const walrusConfigBindings = (structural) => {
75
75
  field("proxyUrl", (i) => i.proxyUrl),
76
76
  field("aggregatorUrl", (i) => i.aggregatorUrl),
77
77
  field("publisherUrl", (i) => i.publisherUrl),
78
+ field("uploadRelayUrl", (i) => i.uploadRelayUrl),
78
79
  nodesBinding
79
80
  ]
80
81
  };
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.mjs","names":[],"sources":["../../../src/plugins/walrus/codegen.ts"],"sourcesContent":["// Walrus plugin — Codegenable contribution, via the UNIFIED config-binding\n// declaration.\n//\n// Walrus's contribution is the SDK-ready `packageConfig` shape that the\n// `@mysten/walrus` SDK consumes — `{systemObjectId, stakingPoolId,\n// exchangeIds}` — plus the proxy / aggregator / publisher URLs for HTTP\n// consumers. Walrus is single-instance per stack, so it exports `walrus`\n// directly (a FLAT bucket, not name-keyed like coin/seal).\n//\n// ONE declaration, TWO derivations (see `contracts/config-bindings.ts`):\n// - LIVE (boot): bakes the resolved ids / URLs into the ephemeral tree AND\n// feeds the generic deployment `values` channel.\n// - STATIC (committed tree): emits `requireValue(dep, 'walrus', '<key>')` so the\n// committed `walrus.ts` carries NO baked object id / endpoint URL.\n//\n// STRUCTURAL fields (`mode`, `network`) stay literals; the on-chain ids,\n// coin type, endpoint URLs, and the storage-node committee are RUNTIME\n// (loaded config data). The composite `packageConfig` / `nodes` values are\n// resolved as whole-value blobs (the array/optional shapes don't split into\n// the flat config-path model).\n\nimport type { CodegenableDecl } from '../../contracts/codegenable.ts';\nimport {\n\tconfigCodegenable,\n\ttype ConfigBinding,\n\ttype ConfigBindingSet,\n} from '../../contracts/config-bindings.ts';\nimport type { JsonValue } from '../../orchestrators/codegen/deployment.ts';\n\n/** Per-node descriptor. */\nexport interface WalrusNodeBinding {\n\treadonly nodeIndex: number;\n\treadonly publicHostname: string;\n\treadonly rpcUrl: string;\n}\n\n/** The typed shape the emitted file exports. */\nexport interface WalrusBindings {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly packageConfig: {\n\t\treadonly systemObjectId: string;\n\t\treadonly stakingPoolId: string;\n\t\treadonly exchangeIds?: ReadonlyArray<string>;\n\t};\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** Inputs to the LIVE codegen contribution — supplied at acquire-time. */\nexport interface MakeCodegenableInputs {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly systemObjectId: string;\n\treadonly stakingPoolId: string;\n\treadonly exchangeIds: ReadonlyArray<string>;\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** User-declared known-deployment ids / URLs, available at factory time. A\n * KNOWN deployment's values are DECLARED config (not loaded-at-runtime data),\n * so the committed `walrus.ts` bakes them as LITERALS. Absent for a `local`\n * (dev-deployed) cluster whose ids/URLs are dynamic. */\nexport interface WalrusKnownConfig {\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly packageConfig: WalrusBindings['packageConfig'];\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** Static-config shape — what walrus knows BEFORE acquire. When `known` is\n * present (known mode), its declared ids/URLs are baked as literals;\n * otherwise (local mode) every id/URL resolves at app build/dev time. */\nexport interface WalrusStaticConfig {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly known?: WalrusKnownConfig;\n}\n\nconst NAMESPACE = 'walrus';\n\n/** TS source-type strings for the resolved walrus fields — keeps the committed\n * `walrus.ts` typed as `WalrusBindings` declares (the generic `requireValue`\n * channel would otherwise return `unknown`). Composite blobs inline their\n * structural literal types so no emitted type-import is needed. */\nconst PACKAGE_CONFIG_TS_TYPE =\n\t'{ readonly systemObjectId: string; readonly stakingPoolId: string; readonly exchangeIds?: ReadonlyArray<string> }';\nconst NODES_TS_TYPE =\n\t'ReadonlyArray<{ readonly nodeIndex: number; readonly publicHostname: string; readonly rpcUrl: string }>';\n\n/** The walrus config bindings, declared ONCE. `mode` / `network` are\n * structural literals; every id / coin type / URL / committee value is a\n * RESOLVED binding on the generic `requireValue(dep, 'walrus', '<key>')` channel.\n * Both the live boot decl and the static committed-tree decl derive from it. */\nconst walrusConfigBindings = (\n\tstructural: WalrusStaticConfig,\n): ConfigBindingSet<MakeCodegenableInputs> => {\n\tconst known = structural.known;\n\t// A known deployment's declared ids/URLs are config (literal); a local\n\t// cluster's are dynamically deployed (resolved at app build/dev time).\n\tconst field = (\n\t\tkey:\n\t\t\t| 'walrusPackageId'\n\t\t\t| 'walPackageId'\n\t\t\t| 'walCoinType'\n\t\t\t| 'proxyUrl'\n\t\t\t| 'aggregatorUrl'\n\t\t\t| 'publisherUrl',\n\t\tlive: (i: MakeCodegenableInputs) => JsonValue,\n\t): ConfigBinding<MakeCodegenableInputs> =>\n\t\tknown !== undefined\n\t\t\t? { variant: 'literal', configPath: [key], value: known[key] }\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: [key],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey,\n\t\t\t\t\ttsType: 'string | null',\n\t\t\t\t\tlive,\n\t\t\t\t};\n\tconst packageConfigBinding: ConfigBinding<MakeCodegenableInputs> =\n\t\tknown !== undefined\n\t\t\t? {\n\t\t\t\t\tvariant: 'literal',\n\t\t\t\t\tconfigPath: ['packageConfig'],\n\t\t\t\t\tvalue: known.packageConfig as JsonValue,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: ['packageConfig'],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey: 'packageConfig',\n\t\t\t\t\ttsType: PACKAGE_CONFIG_TS_TYPE,\n\t\t\t\t\tlive: (i) =>\n\t\t\t\t\t\t({\n\t\t\t\t\t\t\tsystemObjectId: i.systemObjectId,\n\t\t\t\t\t\t\tstakingPoolId: i.stakingPoolId,\n\t\t\t\t\t\t\t...(i.exchangeIds.length > 0 ? { exchangeIds: [...i.exchangeIds] } : {}),\n\t\t\t\t\t\t}) as JsonValue,\n\t\t\t\t};\n\tconst nodesBinding: ConfigBinding<MakeCodegenableInputs> =\n\t\tknown !== undefined\n\t\t\t? { variant: 'literal', configPath: ['nodes'], value: known.nodes as unknown as JsonValue }\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: ['nodes'],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey: 'nodes',\n\t\t\t\t\ttsType: NODES_TS_TYPE,\n\t\t\t\t\tlive: (i) => i.nodes as unknown as JsonValue,\n\t\t\t\t};\n\tconst bindings: ReadonlyArray<ConfigBinding<MakeCodegenableInputs>> = [\n\t\t{ variant: 'literal', configPath: ['mode'], value: structural.mode },\n\t\t{ variant: 'literal', configPath: ['network'], value: structural.network },\n\t\tfield('walrusPackageId', (i) => i.walrusPackageId),\n\t\tfield('walPackageId', (i) => i.walPackageId),\n\t\tfield('walCoinType', (i) => i.walCoinType),\n\t\tpackageConfigBinding,\n\t\tfield('proxyUrl', (i) => i.proxyUrl),\n\t\tfield('aggregatorUrl', (i) => i.aggregatorUrl),\n\t\tfield('publisherUrl', (i) => i.publisherUrl),\n\t\tnodesBinding,\n\t];\n\treturn {\n\t\tbucket: 'walrus.ts',\n\t\tkind: 'walrus',\n\t\temitterName: 'walrus-network',\n\t\tbindings,\n\t};\n};\n\n/** Construct the LIVE Codegenable contribution. Bakes the resolved ids /\n * URLs into the ephemeral tree + feeds the generic deployment `values`\n * channel. */\nexport const makeCodegenable = (inputs: MakeCodegenableInputs): CodegenableDecl =>\n\tconfigCodegenable(walrusConfigBindings({ mode: inputs.mode, network: inputs.network }), {\n\t\tmode: 'live',\n\t\tstate: inputs,\n\t});\n\n/** Construct the STATIC (stack-free) Codegenable contribution. Emits\n * `requireValue(dep, 'walrus', '<key>')` for the runtime fields; the committed\n * `walrus.ts` carries no baked object id / endpoint URL. */\nexport const makeWalrusStaticCodegen = (config: WalrusStaticConfig): CodegenableDecl =>\n\tconfigCodegenable(walrusConfigBindings(config), 'static');\n"],"mappings":";;AA8FA,MAAM,YAAY;;;;;AAMlB,MAAM,yBACL;AACD,MAAM,gBACL;;;;;AAMD,MAAM,wBACL,eAC6C;CAC7C,MAAM,QAAQ,WAAW;CAGzB,MAAM,SACL,KAOA,SAEA,UAAU,KAAA,IACP;EAAE,SAAS;EAAW,YAAY,CAAC,GAAG;EAAG,OAAO,MAAM;CAAK,IAC3D;EACA,SAAS;EACT,YAAY,CAAC,GAAG;EAChB,WAAW;EACX;EACA,QAAQ;EACR;CACD;CACH,MAAM,uBACL,UAAU,KAAA,IACP;EACA,SAAS;EACT,YAAY,CAAC,eAAe;EAC5B,OAAO,MAAM;CACd,IACC;EACA,SAAS;EACT,YAAY,CAAC,eAAe;EAC5B,WAAW;EACX,KAAK;EACL,QAAQ;EACR,OAAO,OACL;GACA,gBAAgB,EAAE;GAClB,eAAe,EAAE;GACjB,GAAI,EAAE,YAAY,SAAS,IAAI,EAAE,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC;EACvE;CACF;CACH,MAAM,eACL,UAAU,KAAA,IACP;EAAE,SAAS;EAAW,YAAY,CAAC,OAAO;EAAG,OAAO,MAAM;CAA8B,IACxF;EACA,SAAS;EACT,YAAY,CAAC,OAAO;EACpB,WAAW;EACX,KAAK;EACL,QAAQ;EACR,OAAO,MAAM,EAAE;CAChB;CAaH,OAAO;EACN,QAAQ;EACR,MAAM;EACN,aAAa;EACb,UAAA;GAfA;IAAE,SAAS;IAAW,YAAY,CAAC,MAAM;IAAG,OAAO,WAAW;GAAK;GACnE;IAAE,SAAS;IAAW,YAAY,CAAC,SAAS;IAAG,OAAO,WAAW;GAAQ;GACzE,MAAM,oBAAoB,MAAM,EAAE,eAAe;GACjD,MAAM,iBAAiB,MAAM,EAAE,YAAY;GAC3C,MAAM,gBAAgB,MAAM,EAAE,WAAW;GACzC;GACA,MAAM,aAAa,MAAM,EAAE,QAAQ;GACnC,MAAM,kBAAkB,MAAM,EAAE,aAAa;GAC7C,MAAM,iBAAiB,MAAM,EAAE,YAAY;GAC3C;EAMO;CACR;AACD;;;;AAKA,MAAa,mBAAmB,WAC/B,kBAAkB,qBAAqB;CAAE,MAAM,OAAO;CAAM,SAAS,OAAO;AAAQ,CAAC,GAAG;CACvF,MAAM;CACN,OAAO;AACR,CAAC;;;;AAKF,MAAa,2BAA2B,WACvC,kBAAkB,qBAAqB,MAAM,GAAG,QAAQ"}
1
+ {"version":3,"file":"codegen.mjs","names":[],"sources":["../../../src/plugins/walrus/codegen.ts"],"sourcesContent":["// Walrus plugin — Codegenable contribution, via the UNIFIED config-binding\n// declaration.\n//\n// Walrus's contribution is the SDK-ready `packageConfig` shape that the\n// `@mysten/walrus` SDK consumes — `{systemObjectId, stakingPoolId,\n// exchangeIds}` — plus the proxy / aggregator / publisher / upload-relay URLs\n// for HTTP consumers. Walrus is single-instance per stack, so it exports `walrus`\n// directly (a FLAT bucket, not name-keyed like coin/seal).\n//\n// ONE declaration, TWO derivations (see `contracts/config-bindings.ts`):\n// - LIVE (boot): bakes the resolved ids / URLs into the ephemeral tree AND\n// feeds the generic deployment `values` channel.\n// - STATIC (committed tree): emits `requireValue(dep, 'walrus', '<key>')` so the\n// committed `walrus.ts` carries NO baked object id / endpoint URL.\n//\n// STRUCTURAL fields (`mode`, `network`) stay literals; the on-chain ids,\n// coin type, endpoint URLs, and the storage-node committee are RUNTIME\n// (loaded config data). The composite `packageConfig` / `nodes` values are\n// resolved as whole-value blobs (the array/optional shapes don't split into\n// the flat config-path model).\n\nimport type { CodegenableDecl } from '../../contracts/codegenable.ts';\nimport {\n\tconfigCodegenable,\n\ttype ConfigBinding,\n\ttype ConfigBindingSet,\n} from '../../contracts/config-bindings.ts';\nimport type { JsonValue } from '../../orchestrators/codegen/deployment.ts';\n\n/** Per-node descriptor. */\nexport interface WalrusNodeBinding {\n\treadonly nodeIndex: number;\n\treadonly publicHostname: string;\n\treadonly rpcUrl: string;\n}\n\n/** The typed shape the emitted file exports. */\nexport interface WalrusBindings {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly packageConfig: {\n\t\treadonly systemObjectId: string;\n\t\treadonly stakingPoolId: string;\n\t\treadonly exchangeIds?: ReadonlyArray<string>;\n\t};\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly uploadRelayUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** Inputs to the LIVE codegen contribution — supplied at acquire-time. */\nexport interface MakeCodegenableInputs {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly systemObjectId: string;\n\treadonly stakingPoolId: string;\n\treadonly exchangeIds: ReadonlyArray<string>;\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly uploadRelayUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** User-declared known-deployment ids / URLs, available at factory time. A\n * KNOWN deployment's values are DECLARED config (not loaded-at-runtime data),\n * so the committed `walrus.ts` bakes them as LITERALS. Absent for a `local`\n * (dev-deployed) cluster whose ids/URLs are dynamic. */\nexport interface WalrusKnownConfig {\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly packageConfig: WalrusBindings['packageConfig'];\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly uploadRelayUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** Static-config shape — what walrus knows BEFORE acquire. When `known` is\n * present (known mode), its declared ids/URLs are baked as literals;\n * otherwise (local mode) every id/URL resolves at app build/dev time. */\nexport interface WalrusStaticConfig {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly known?: WalrusKnownConfig;\n}\n\nconst NAMESPACE = 'walrus';\n\n/** TS source-type strings for the resolved walrus fields — keeps the committed\n * `walrus.ts` typed as `WalrusBindings` declares (the generic `requireValue`\n * channel would otherwise return `unknown`). Composite blobs inline their\n * structural literal types so no emitted type-import is needed. */\nconst PACKAGE_CONFIG_TS_TYPE =\n\t'{ readonly systemObjectId: string; readonly stakingPoolId: string; readonly exchangeIds?: ReadonlyArray<string> }';\nconst NODES_TS_TYPE =\n\t'ReadonlyArray<{ readonly nodeIndex: number; readonly publicHostname: string; readonly rpcUrl: string }>';\n\n/** The walrus config bindings, declared ONCE. `mode` / `network` are\n * structural literals; every id / coin type / URL / committee value is a\n * RESOLVED binding on the generic `requireValue(dep, 'walrus', '<key>')` channel.\n * Both the live boot decl and the static committed-tree decl derive from it. */\nconst walrusConfigBindings = (\n\tstructural: WalrusStaticConfig,\n): ConfigBindingSet<MakeCodegenableInputs> => {\n\tconst known = structural.known;\n\t// A known deployment's declared ids/URLs are config (literal); a local\n\t// cluster's are dynamically deployed (resolved at app build/dev time).\n\tconst field = (\n\t\tkey:\n\t\t\t| 'walrusPackageId'\n\t\t\t| 'walPackageId'\n\t\t\t| 'walCoinType'\n\t\t\t| 'proxyUrl'\n\t\t\t| 'aggregatorUrl'\n\t\t\t| 'publisherUrl'\n\t\t\t| 'uploadRelayUrl',\n\t\tlive: (i: MakeCodegenableInputs) => JsonValue,\n\t): ConfigBinding<MakeCodegenableInputs> =>\n\t\tknown !== undefined\n\t\t\t? { variant: 'literal', configPath: [key], value: known[key] }\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: [key],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey,\n\t\t\t\t\ttsType: 'string | null',\n\t\t\t\t\tlive,\n\t\t\t\t};\n\tconst packageConfigBinding: ConfigBinding<MakeCodegenableInputs> =\n\t\tknown !== undefined\n\t\t\t? {\n\t\t\t\t\tvariant: 'literal',\n\t\t\t\t\tconfigPath: ['packageConfig'],\n\t\t\t\t\tvalue: known.packageConfig as JsonValue,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: ['packageConfig'],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey: 'packageConfig',\n\t\t\t\t\ttsType: PACKAGE_CONFIG_TS_TYPE,\n\t\t\t\t\tlive: (i) =>\n\t\t\t\t\t\t({\n\t\t\t\t\t\t\tsystemObjectId: i.systemObjectId,\n\t\t\t\t\t\t\tstakingPoolId: i.stakingPoolId,\n\t\t\t\t\t\t\t...(i.exchangeIds.length > 0 ? { exchangeIds: [...i.exchangeIds] } : {}),\n\t\t\t\t\t\t}) as JsonValue,\n\t\t\t\t};\n\tconst nodesBinding: ConfigBinding<MakeCodegenableInputs> =\n\t\tknown !== undefined\n\t\t\t? { variant: 'literal', configPath: ['nodes'], value: known.nodes as unknown as JsonValue }\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: ['nodes'],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey: 'nodes',\n\t\t\t\t\ttsType: NODES_TS_TYPE,\n\t\t\t\t\tlive: (i) => i.nodes as unknown as JsonValue,\n\t\t\t\t};\n\tconst bindings: ReadonlyArray<ConfigBinding<MakeCodegenableInputs>> = [\n\t\t{ variant: 'literal', configPath: ['mode'], value: structural.mode },\n\t\t{ variant: 'literal', configPath: ['network'], value: structural.network },\n\t\tfield('walrusPackageId', (i) => i.walrusPackageId),\n\t\tfield('walPackageId', (i) => i.walPackageId),\n\t\tfield('walCoinType', (i) => i.walCoinType),\n\t\tpackageConfigBinding,\n\t\tfield('proxyUrl', (i) => i.proxyUrl),\n\t\tfield('aggregatorUrl', (i) => i.aggregatorUrl),\n\t\tfield('publisherUrl', (i) => i.publisherUrl),\n\t\tfield('uploadRelayUrl', (i) => i.uploadRelayUrl),\n\t\tnodesBinding,\n\t];\n\treturn {\n\t\tbucket: 'walrus.ts',\n\t\tkind: 'walrus',\n\t\temitterName: 'walrus-network',\n\t\tbindings,\n\t};\n};\n\n/** Construct the LIVE Codegenable contribution. Bakes the resolved ids /\n * URLs into the ephemeral tree + feeds the generic deployment `values`\n * channel. */\nexport const makeCodegenable = (inputs: MakeCodegenableInputs): CodegenableDecl =>\n\tconfigCodegenable(walrusConfigBindings({ mode: inputs.mode, network: inputs.network }), {\n\t\tmode: 'live',\n\t\tstate: inputs,\n\t});\n\n/** Construct the STATIC (stack-free) Codegenable contribution. Emits\n * `requireValue(dep, 'walrus', '<key>')` for the runtime fields; the committed\n * `walrus.ts` carries no baked object id / endpoint URL. */\nexport const makeWalrusStaticCodegen = (config: WalrusStaticConfig): CodegenableDecl =>\n\tconfigCodegenable(walrusConfigBindings(config), 'static');\n"],"mappings":";;AAiGA,MAAM,YAAY;;;;;AAMlB,MAAM,yBACL;AACD,MAAM,gBACL;;;;;AAMD,MAAM,wBACL,eAC6C;CAC7C,MAAM,QAAQ,WAAW;CAGzB,MAAM,SACL,KAQA,SAEA,UAAU,KAAA,IACP;EAAE,SAAS;EAAW,YAAY,CAAC,GAAG;EAAG,OAAO,MAAM;CAAK,IAC3D;EACA,SAAS;EACT,YAAY,CAAC,GAAG;EAChB,WAAW;EACX;EACA,QAAQ;EACR;CACD;CACH,MAAM,uBACL,UAAU,KAAA,IACP;EACA,SAAS;EACT,YAAY,CAAC,eAAe;EAC5B,OAAO,MAAM;CACd,IACC;EACA,SAAS;EACT,YAAY,CAAC,eAAe;EAC5B,WAAW;EACX,KAAK;EACL,QAAQ;EACR,OAAO,OACL;GACA,gBAAgB,EAAE;GAClB,eAAe,EAAE;GACjB,GAAI,EAAE,YAAY,SAAS,IAAI,EAAE,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC;EACvE;CACF;CACH,MAAM,eACL,UAAU,KAAA,IACP;EAAE,SAAS;EAAW,YAAY,CAAC,OAAO;EAAG,OAAO,MAAM;CAA8B,IACxF;EACA,SAAS;EACT,YAAY,CAAC,OAAO;EACpB,WAAW;EACX,KAAK;EACL,QAAQ;EACR,OAAO,MAAM,EAAE;CAChB;CAcH,OAAO;EACN,QAAQ;EACR,MAAM;EACN,aAAa;EACb,UAAA;GAhBA;IAAE,SAAS;IAAW,YAAY,CAAC,MAAM;IAAG,OAAO,WAAW;GAAK;GACnE;IAAE,SAAS;IAAW,YAAY,CAAC,SAAS;IAAG,OAAO,WAAW;GAAQ;GACzE,MAAM,oBAAoB,MAAM,EAAE,eAAe;GACjD,MAAM,iBAAiB,MAAM,EAAE,YAAY;GAC3C,MAAM,gBAAgB,MAAM,EAAE,WAAW;GACzC;GACA,MAAM,aAAa,MAAM,EAAE,QAAQ;GACnC,MAAM,kBAAkB,MAAM,EAAE,aAAa;GAC7C,MAAM,iBAAiB,MAAM,EAAE,YAAY;GAC3C,MAAM,mBAAmB,MAAM,EAAE,cAAc;GAC/C;EAMO;CACR;AACD;;;;AAKA,MAAa,mBAAmB,WAC/B,kBAAkB,qBAAqB;CAAE,MAAM,OAAO;CAAM,SAAS,OAAO;AAAQ,CAAC,GAAG;CACvF,MAAM;CACN,OAAO;AACR,CAAC;;;;AAKF,MAAa,2BAA2B,WACvC,kBAAkB,qBAAqB,MAAM,GAAG,QAAQ"}
@@ -9,7 +9,7 @@ import { ForkIncompatibleError } from "../../substrate/runtime/mode-errors.mjs";
9
9
  * creation now, so its errors surface as `ContainerRuntimeError`
10
10
  * upstream — we narrate the *phase* as `'cluster-network'` but the
11
11
  * fault tags through the substrate). */
12
- type WalrusPhase = 'image-build' | 'cluster-network' | 'deploy' | 'aggregator' | 'publisher' | 'exchange' | 'storage-node' | 'proxy' | 'fund-wal' | 'register-known';
12
+ type WalrusPhase = 'image-build' | 'cluster-network' | 'deploy' | 'aggregator' | 'publisher' | 'upload-relay' | 'exchange' | 'storage-node' | 'proxy' | 'fund-wal' | 'register-known';
13
13
  /** Generic Walrus plugin error. Raised by the plugin's acquire
14
14
  * body, WAL funding strategy, and the per-mode builders. */
15
15
  interface WalrusPluginError {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../../../src/plugins/walrus/errors.ts"],"sourcesContent":["// Walrus plugin — typed errors.\n//\n// Errors raised and consumed inside the Walrus plugin live here.\n// Cross-service errors that Walrus *consumes* but the substrate\n// raises (e.g. `ArtifactPublishError`) come from the substrate's\n// primitive — we don't redeclare those.\n//\n// `ForkIncompatibleError` is a cross-cutting mode-refusal shape\n// owned by `substrate/runtime/mode-errors.ts`; walrus contributes\n// the `walrusLocalCluster` variant via the factory below.\n//\n// Effect v4: plain interfaces with `_tag` discriminator (per\n// surrounding subsystem style). `Effect.catchTag` matches on `_tag`.\n\nimport { ForkIncompatibleError } from '../../substrate/runtime/mode-errors.ts';\nimport { defineConfigError, type ConfigIssue } from '../../substrate/runtime/config-validation.ts';\n\nexport { ForkIncompatibleError };\n\n/** Phases for `WalrusError`. Closed sum — keeps the cause-walker's\n * display table small. Matches the closed `WalrusPhases` from the\n * distilled doc §\"Cross-component references\" except `'network'`\n * is dropped (the substrate's `ContainerRuntime` owns docker network\n * creation now, so its errors surface as `ContainerRuntimeError`\n * upstream — we narrate the *phase* as `'cluster-network'` but the\n * fault tags through the substrate). */\nexport type WalrusPhase =\n\t| 'image-build'\n\t| 'cluster-network'\n\t| 'deploy'\n\t| 'aggregator'\n\t| 'publisher'\n\t| 'exchange'\n\t| 'storage-node'\n\t| 'proxy'\n\t| 'fund-wal'\n\t| 'register-known';\n\n/** Generic Walrus plugin error. Raised by the plugin's acquire\n * body, WAL funding strategy, and the per-mode builders. */\nexport interface WalrusPluginError {\n\treadonly _tag: 'WalrusPluginError';\n\treadonly phase: WalrusPhase;\n\treadonly message: string;\n\treadonly cause?: unknown;\n\t/** Optional sub-process capture envelope — populated for deploy\n\t * one-shot + per-node container failures. */\n\treadonly stderr?: string;\n\treadonly stdout?: string;\n\treadonly exitCode?: number;\n}\n\nexport const walrusPluginError = (\n\tphase: WalrusPhase,\n\tmessage: string,\n\tparts: Omit<WalrusPluginError, '_tag' | 'phase' | 'message'> = {},\n): WalrusPluginError => ({ _tag: 'WalrusPluginError', phase, message, ...parts });\n\n/** Configuration error — synchronous factory-time guards\n * (`nodeCount >= 1`, `shards >= nodeCount`, missing required\n * fields on `.known(...)`). Surfaces as a thrown `Error` shaped\n * like this in the factory, mirroring the distilled-doc behavior\n * of synchronous configuration faults. */\nexport interface WalrusConfigError extends ConfigIssue {\n\treadonly _tag: 'WalrusConfigError';\n}\n\nconst makeWalrusConfigError = defineConfigError('WalrusConfigError');\n\nexport const walrusConfigError = (\n\tfield: string,\n\tmessage: string,\n\thint?: string,\n\tcause?: unknown,\n): WalrusConfigError => makeWalrusConfigError({ field, message, hint, cause });\n\n/** Union of every error a Walrus-plugin caller may encounter. */\nexport type WalrusError = WalrusPluginError | ForkIncompatibleError | WalrusConfigError;\n\n/** The catchable error tags this plugin exposes. Pinned against the\n * user-facing error catalog by the error-catalog-parity test. */\nexport const WALRUS_ERROR_TAGS: ReadonlyArray<WalrusError['_tag']> = [\n\t'WalrusPluginError',\n\t'ForkIncompatibleError',\n\t'WalrusConfigError',\n] as const;\n"],"mappings":";;;AAoDA,MAAa,qBACZ,OACA,SACA,QAA+D,CAAC,OACxC;CAAE,MAAM;CAAqB;CAAO;CAAS,GAAG;AAAM;AAW/E,MAAM,wBAAwB,kBAAkB,mBAAmB;AAEnE,MAAa,qBACZ,OACA,SACA,MACA,UACuB,sBAAsB;CAAE;CAAO;CAAS;CAAM;AAAM,CAAC"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../../src/plugins/walrus/errors.ts"],"sourcesContent":["// Walrus plugin — typed errors.\n//\n// Errors raised and consumed inside the Walrus plugin live here.\n// Cross-service errors that Walrus *consumes* but the substrate\n// raises (e.g. `ArtifactPublishError`) come from the substrate's\n// primitive — we don't redeclare those.\n//\n// `ForkIncompatibleError` is a cross-cutting mode-refusal shape\n// owned by `substrate/runtime/mode-errors.ts`; walrus contributes\n// the `walrusLocalCluster` variant via the factory below.\n//\n// Effect v4: plain interfaces with `_tag` discriminator (per\n// surrounding subsystem style). `Effect.catchTag` matches on `_tag`.\n\nimport { ForkIncompatibleError } from '../../substrate/runtime/mode-errors.ts';\nimport { defineConfigError, type ConfigIssue } from '../../substrate/runtime/config-validation.ts';\n\nexport { ForkIncompatibleError };\n\n/** Phases for `WalrusError`. Closed sum — keeps the cause-walker's\n * display table small. Matches the closed `WalrusPhases` from the\n * distilled doc §\"Cross-component references\" except `'network'`\n * is dropped (the substrate's `ContainerRuntime` owns docker network\n * creation now, so its errors surface as `ContainerRuntimeError`\n * upstream — we narrate the *phase* as `'cluster-network'` but the\n * fault tags through the substrate). */\nexport type WalrusPhase =\n\t| 'image-build'\n\t| 'cluster-network'\n\t| 'deploy'\n\t| 'aggregator'\n\t| 'publisher'\n\t| 'upload-relay'\n\t| 'exchange'\n\t| 'storage-node'\n\t| 'proxy'\n\t| 'fund-wal'\n\t| 'register-known';\n\n/** Generic Walrus plugin error. Raised by the plugin's acquire\n * body, WAL funding strategy, and the per-mode builders. */\nexport interface WalrusPluginError {\n\treadonly _tag: 'WalrusPluginError';\n\treadonly phase: WalrusPhase;\n\treadonly message: string;\n\treadonly cause?: unknown;\n\t/** Optional sub-process capture envelope — populated for deploy\n\t * one-shot + per-node container failures. */\n\treadonly stderr?: string;\n\treadonly stdout?: string;\n\treadonly exitCode?: number;\n}\n\nexport const walrusPluginError = (\n\tphase: WalrusPhase,\n\tmessage: string,\n\tparts: Omit<WalrusPluginError, '_tag' | 'phase' | 'message'> = {},\n): WalrusPluginError => ({ _tag: 'WalrusPluginError', phase, message, ...parts });\n\n/** Configuration error — synchronous factory-time guards\n * (`nodeCount >= 1`, `shards >= nodeCount`, missing required\n * fields on `.known(...)`). Surfaces as a thrown `Error` shaped\n * like this in the factory, mirroring the distilled-doc behavior\n * of synchronous configuration faults. */\nexport interface WalrusConfigError extends ConfigIssue {\n\treadonly _tag: 'WalrusConfigError';\n}\n\nconst makeWalrusConfigError = defineConfigError('WalrusConfigError');\n\nexport const walrusConfigError = (\n\tfield: string,\n\tmessage: string,\n\thint?: string,\n\tcause?: unknown,\n): WalrusConfigError => makeWalrusConfigError({ field, message, hint, cause });\n\n/** Union of every error a Walrus-plugin caller may encounter. */\nexport type WalrusError = WalrusPluginError | ForkIncompatibleError | WalrusConfigError;\n\n/** The catchable error tags this plugin exposes. Pinned against the\n * user-facing error catalog by the error-catalog-parity test. */\nexport const WALRUS_ERROR_TAGS: ReadonlyArray<WalrusError['_tag']> = [\n\t'WalrusPluginError',\n\t'ForkIncompatibleError',\n\t'WalrusConfigError',\n] as const;\n"],"mappings":";;;AAqDA,MAAa,qBACZ,OACA,SACA,QAA+D,CAAC,OACxC;CAAE,MAAM;CAAqB;CAAO;CAAS,GAAG;AAAM;AAW/E,MAAM,wBAAwB,kBAAkB,mBAAmB;AAEnE,MAAa,qBACZ,OACA,SACA,MACA,UACuB,sBAAsB;CAAE;CAAO;CAAS;CAAM;AAAM,CAAC"}
@@ -28,6 +28,7 @@ interface WalrusResolved {
28
28
  readonly proxyUrl: string | null;
29
29
  readonly aggregatorUrl: string | null;
30
30
  readonly publisherUrl: string | null;
31
+ readonly uploadRelayUrl: string | null;
31
32
  readonly walFaucetStrategy: WalFaucetStrategy | null;
32
33
  readonly walCoinType: string | null;
33
34
  }
@@ -96,6 +97,7 @@ declare const walrusFor: ModeNamespace<{
96
97
  proxyUrl: string | null;
97
98
  aggregatorUrl: string | null;
98
99
  publisherUrl: string | null;
100
+ uploadRelayUrl: string | null;
99
101
  walFaucetStrategy: null;
100
102
  walCoinType: null;
101
103
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -115,6 +117,7 @@ declare const walrusFor: ModeNamespace<{
115
117
  proxyUrl: string | null;
116
118
  aggregatorUrl: string | null;
117
119
  publisherUrl: string | null;
120
+ uploadRelayUrl: string | null;
118
121
  walFaucetStrategy: null;
119
122
  walCoinType: null;
120
123
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -134,6 +137,7 @@ declare const walrusFor: ModeNamespace<{
134
137
  proxyUrl: string | null;
135
138
  aggregatorUrl: string | null;
136
139
  publisherUrl: string | null;
140
+ uploadRelayUrl: string | null;
137
141
  walFaucetStrategy: null;
138
142
  walCoinType: null;
139
143
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -155,6 +159,7 @@ declare const walrusFor: ModeNamespace<{
155
159
  proxyUrl: string | null;
156
160
  aggregatorUrl: string | null;
157
161
  publisherUrl: string | null;
162
+ uploadRelayUrl: string | null;
158
163
  walFaucetStrategy: null;
159
164
  walCoinType: null;
160
165
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -174,6 +179,7 @@ declare const walrusFor: ModeNamespace<{
174
179
  proxyUrl: string | null;
175
180
  aggregatorUrl: string | null;
176
181
  publisherUrl: string | null;
182
+ uploadRelayUrl: string | null;
177
183
  walFaucetStrategy: null;
178
184
  walCoinType: null;
179
185
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -193,6 +199,7 @@ declare const walrusFor: ModeNamespace<{
193
199
  proxyUrl: string | null;
194
200
  aggregatorUrl: string | null;
195
201
  publisherUrl: string | null;
202
+ uploadRelayUrl: string | null;
196
203
  walFaucetStrategy: null;
197
204
  walCoinType: null;
198
205
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -214,6 +221,7 @@ declare const walrusFor: ModeNamespace<{
214
221
  proxyUrl: string | null;
215
222
  aggregatorUrl: string | null;
216
223
  publisherUrl: string | null;
224
+ uploadRelayUrl: string | null;
217
225
  walFaucetStrategy: null;
218
226
  walCoinType: null;
219
227
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -233,6 +241,7 @@ declare const walrusFor: ModeNamespace<{
233
241
  proxyUrl: string | null;
234
242
  aggregatorUrl: string | null;
235
243
  publisherUrl: string | null;
244
+ uploadRelayUrl: string | null;
236
245
  walFaucetStrategy: null;
237
246
  walCoinType: null;
238
247
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -252,6 +261,7 @@ declare const walrusFor: ModeNamespace<{
252
261
  proxyUrl: string | null;
253
262
  aggregatorUrl: string | null;
254
263
  publisherUrl: string | null;
264
+ uploadRelayUrl: string | null;
255
265
  walFaucetStrategy: null;
256
266
  walCoinType: null;
257
267
  }, readonly [ResourceRef<"sui", SuiClient & {
@@ -11,7 +11,7 @@ import { LOCAL_NETWORK_NAME } from "../../api/inference-network.mjs";
11
11
  import { renderUrl, routedHostname } from "../../substrate/runtime/routed-url.mjs";
12
12
  import { walrusPluginError } from "./errors.mjs";
13
13
  import { WALRUS_ROUTER_PORT, buildWalrusNetworkName } from "./storage-nodes.mjs";
14
- import { WALRUS_AGGREGATOR_ENDPOINT_NAME, WALRUS_PUBLISHER_ENDPOINT_NAME, makeLocalRoutables } from "./routable.mjs";
14
+ import { WALRUS_AGGREGATOR_ENDPOINT_NAME, WALRUS_PUBLISHER_ENDPOINT_NAME, WALRUS_UPLOAD_RELAY_ENDPOINT_NAME, makeLocalRoutables } from "./routable.mjs";
15
15
  import { defineModeNamespace } from "../../api/mode-narrowed-factory.mjs";
16
16
  import { suiResource } from "../sui/index.mjs";
17
17
  import { coinResourceId } from "../coin/index.mjs";
@@ -110,6 +110,7 @@ const buildLocalPlugin = (opts) => {
110
110
  })), Effect.mapError((cause) => walrusPluginError("proxy", `walrus route URL assembly failed for ${role}: ${cause.detail}`, { cause })));
111
111
  const aggregatorUrl = boot.clientServices.aggregator === null ? null : yield* serviceUrl(WALRUS_AGGREGATOR_ENDPOINT_NAME);
112
112
  const publisherUrl = boot.clientServices.publisher === null ? null : yield* serviceUrl(WALRUS_PUBLISHER_ENDPOINT_NAME);
113
+ const uploadRelayUrl = boot.clientServices.uploadRelay === null ? null : yield* serviceUrl(WALRUS_UPLOAD_RELAY_ENDPOINT_NAME);
113
114
  const resolvedValue = {
114
115
  mode: "local",
115
116
  network: identity.network,
@@ -120,11 +121,16 @@ const buildLocalPlugin = (opts) => {
120
121
  proxyUrl: aggregatorUrl,
121
122
  aggregatorUrl,
122
123
  publisherUrl,
124
+ uploadRelayUrl,
123
125
  walFaucetStrategy: boot.walFaucetStrategy,
124
126
  walCoinType: boot.walCoinType
125
127
  };
126
128
  const walFaucetContribution = resolvedValue.walFaucetStrategy === null || resolvedValue.walCoinType === null ? [] : [makeWalFaucetContribution(resolvedValue.walFaucetStrategy, resolvedValue.walCoinType)];
127
- const clientServiceRoles = [...boot.clientServices.aggregator === null ? [] : ["aggregator"], ...boot.clientServices.publisher === null ? [] : ["publisher"]];
129
+ const clientServiceRoles = [
130
+ ...boot.clientServices.aggregator === null ? [] : ["aggregator"],
131
+ ...boot.clientServices.publisher === null ? [] : ["publisher"],
132
+ ...boot.clientServices.uploadRelay === null ? [] : ["upload-relay"]
133
+ ];
128
134
  emitContributions(ctx, [
129
135
  makeSnapshotable("local", identity.app, identity.stack, resolved.name, resolvedValue.network, resolved.nodeCount, clientServiceRoles),
130
136
  makeCodegenable({
@@ -139,6 +145,7 @@ const buildLocalPlugin = (opts) => {
139
145
  proxyUrl: resolvedValue.proxyUrl,
140
146
  aggregatorUrl: resolvedValue.aggregatorUrl,
141
147
  publisherUrl: resolvedValue.publisherUrl,
148
+ uploadRelayUrl: resolvedValue.uploadRelayUrl,
142
149
  nodes: resolvedValue.nodes
143
150
  }),
144
151
  {
@@ -156,7 +163,8 @@ const buildLocalPlugin = (opts) => {
156
163
  nodeCount: resolved.nodeCount,
157
164
  containerApiPort: resolved.containerApiPort,
158
165
  aggregator: boot.clientServices.aggregator,
159
- publisher: boot.clientServices.publisher
166
+ publisher: boot.clientServices.publisher,
167
+ uploadRelay: boot.clientServices.uploadRelay
160
168
  })
161
169
  ]);
162
170
  return resolvedValue;
@@ -185,6 +193,7 @@ const buildKnownPlugin = (opts) => {
185
193
  proxyUrl: resolved.proxyUrl,
186
194
  aggregatorUrl: resolved.aggregatorUrl,
187
195
  publisherUrl: resolved.publisherUrl,
196
+ uploadRelayUrl: resolved.uploadRelayUrl,
188
197
  nodes: resolved.nodes
189
198
  }
190
199
  })],
@@ -205,6 +214,7 @@ const buildKnownPlugin = (opts) => {
205
214
  proxyUrl: resolved.proxyUrl,
206
215
  aggregatorUrl: resolved.aggregatorUrl,
207
216
  publisherUrl: resolved.publisherUrl,
217
+ uploadRelayUrl: resolved.uploadRelayUrl,
208
218
  walFaucetStrategy: null,
209
219
  walCoinType: null
210
220
  };
@@ -221,6 +231,7 @@ const buildKnownPlugin = (opts) => {
221
231
  proxyUrl: resolvedValue.proxyUrl,
222
232
  aggregatorUrl: resolvedValue.aggregatorUrl,
223
233
  publisherUrl: resolvedValue.publisherUrl,
234
+ uploadRelayUrl: resolvedValue.uploadRelayUrl,
224
235
  nodes: resolvedValue.nodes
225
236
  }));
226
237
  ctx.provides({