everything-dev 2.0.0-rc.5 → 2.0.0-rc.6

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.
@@ -95,6 +95,40 @@ const staleResponse = (bytes, url) => {
95
95
  }
96
96
  });
97
97
  };
98
+ const FETCH_RETRY_ATTEMPTS = 3;
99
+ const FETCH_RETRY_BASE_DELAY_MS = 250;
100
+ const isTransientStatus = (status) => status === 429 || status >= 500;
101
+ function requestMethod(input, init) {
102
+ if (init?.method) return init.method.toUpperCase();
103
+ if (typeof input === "string" || input instanceof URL) return "GET";
104
+ return input.method.toUpperCase();
105
+ }
106
+ /**
107
+ * Bounded transient-failure retry for idempotent GETs crossing the
108
+ * interceptor (ADR 0021 amendment — the CDN-only image): a cold boot has no
109
+ * staged fallback, so one edge/registry hiccup during the load window would
110
+ * hard-fail the host or a plugin with no recourse. Network errors and
111
+ * 429/5xx responses retry twice (250ms, 750ms backoff) before propagating;
112
+ * non-transient statuses and non-GET requests pass through untouched. After
113
+ * exhausting attempts a stale-if-error cache still sees the final failure.
114
+ */
115
+ async function fetchWithTransientRetry(original, input, init) {
116
+ if (requestMethod(input, init) !== "GET") return original(input, init);
117
+ let lastError;
118
+ let lastResponse = null;
119
+ for (let attempt = 0; attempt < FETCH_RETRY_ATTEMPTS; attempt++) {
120
+ if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, FETCH_RETRY_BASE_DELAY_MS * 3 ** (attempt - 1)));
121
+ try {
122
+ const response = await original(input, init);
123
+ if (!isTransientStatus(response.status)) return response;
124
+ lastResponse = response;
125
+ } catch (error) {
126
+ lastError = error;
127
+ }
128
+ }
129
+ if (lastResponse) return lastResponse;
130
+ throw lastError;
131
+ }
98
132
  /**
99
133
  * Installs the resolver as a global fetch interceptor — the one seam every
100
134
  * boot-time consumer shares (config manifest discovery, contract-type
@@ -121,11 +155,11 @@ function installGlobalBundleFetch(options) {
121
155
  return yield* (yield* BundleResolver).lookup(input);
122
156
  }));
123
157
  if (response !== null) return response;
124
- return original(input, init);
158
+ return fetchWithTransientRetry(original, input, init);
125
159
  }
126
160
  if (cacheDir !== void 0 && require_bundle_cache.bundleCachePath(url, { cacheDir })) return (async () => {
127
161
  try {
128
- const response = await original(input, init);
162
+ const response = await fetchWithTransientRetry(original, input, init);
129
163
  if (response.ok) try {
130
164
  const bytes = new Uint8Array(await response.clone().arrayBuffer());
131
165
  await require_bundle_cache.writeBundleCache(url, bytes, { cacheDir });
@@ -141,7 +175,7 @@ function installGlobalBundleFetch(options) {
141
175
  throw error;
142
176
  }
143
177
  })();
144
- return original(input, init);
178
+ return fetchWithTransientRetry(original, input, init);
145
179
  });
146
180
  const originalPreconnect = original.preconnect;
147
181
  if (originalPreconnect) patched.preconnect = originalPreconnect.bind(original);
@@ -1 +1 @@
1
- {"version":3,"file":"bundle-fs-resolve.cjs","names":["Schema","bundleUrlToStagedPath","path","Context","Effect","readFile","cacheControlOf","Layer","ManagedRuntime","bundleCachePath","writeBundleCache","readBundleCache","isAppDescriptorPath","readFileSync","existsSync","bundleCacheRoot"],"sources":["../src/bundle-fs-resolve.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Context, Effect, Layer, ManagedRuntime, Schema } from \"effect\";\nimport { cacheControlOf } from \"every-plugin/build/artifact-names\";\nimport {\n bundleCachePath,\n bundleCacheRoot,\n readBundleCache,\n writeBundleCache,\n} from \"./bundle-cache\";\nimport { bundleUrlToStagedPath } from \"./bundle-path\";\nimport { isAppDescriptorPath } from \"./config\";\n\n/**\n * Local-first bundle resolution (ADR 0011 amendment; unified policy, ADR 0021\n * amendment): a self-contained runtime image stages its own namespace under\n * `BOS_BUNDLE_DIR` as `bundles/<account>/<gateway>/<workspace>/…` and consumes\n * those bytes directly from disk; a miss falls through to the network, so a\n * partially-staged namespace boots over the wire instead of failing. The\n * registry tier (children, `BOS_BUNDLE_DIR` unset) keeps the network fetch —\n * the namespace guard below only ever matches the runtime's own\n * account/gateway, never another runtime's layout.\n */\n\nexport interface BundleNamespace {\n readonly bundleDir: string;\n readonly account: string;\n readonly gateway: string;\n}\n\nexport class BundleReadError extends Schema.TaggedError<BundleReadError>()(\"BundleReadError\", {\n path: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n}) {}\n\nconst MIME_TYPES: Record<string, string> = {\n \".js\": \"text/javascript\",\n \".mjs\": \"text/javascript\",\n \".cjs\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".map\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".ico\": \"image/x-icon\",\n \".html\": \"text/html\",\n \".htm\": \"text/html\",\n \".webmanifest\": \"application/manifest+json\",\n \".md\": \"text/markdown\",\n \".ts\": \"text/plain\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".otf\": \"font/otf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".wasm\": \"application/wasm\",\n \".txt\": \"text/plain\",\n \".xml\": \"application/xml\",\n};\n\n/**\n * Own-namespace path for a bundle URL: the shared URL→staged-path mapping,\n * narrowed to the runtime identity — a URL whose encoded namespace differs\n * from `namespace.account`/`namespace.gateway` must fall to the network/cache\n * path, never serve a sibling namespace's staged bytes.\n */\nexport function bundleUrlToLocalPath(url: string, namespace: BundleNamespace): string | null {\n const filePath = bundleUrlToStagedPath(url, namespace.bundleDir);\n if (!filePath) return null;\n\n const nsDir = path.resolve(namespace.bundleDir, namespace.account, namespace.gateway);\n if (filePath !== nsDir && !filePath.startsWith(nsDir + path.sep)) return null;\n\n return filePath;\n}\n\nexport class BundleResolver extends Context.Service<\n BundleResolver,\n {\n /** Resolve a fetch target from the staged namespace; null = miss or not our namespace — fall through to the network. */\n readonly lookup: (input: string | URL | Request) => Effect.Effect<Response | null>;\n }\n>()(\"everything-dev/bundle-fs-resolve/BundleResolver\") {\n static layer(namespace: BundleNamespace): Layer.Layer<BundleResolver> {\n const respond = Effect.fn(\"BundleResolver.respond\")(function* (\n url: string,\n ): Effect.fn.Return<Response | null, never> {\n const filePath = bundleUrlToLocalPath(url, namespace);\n if (!filePath) return null;\n const bytes = yield* Effect.tryPromise({\n try: () => readFile(filePath),\n catch: (cause) => new BundleReadError({ path: filePath, cause }),\n }).pipe(Effect.catchTag(\"BundleReadError\", () => Effect.succeed(null)));\n if (bytes === null) return null;\n const name = path.basename(filePath);\n const contentType =\n MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(new Uint8Array(bytes), {\n headers: {\n \"content-type\": contentType,\n \"cache-control\": cacheControlOf(name),\n etag: `\"${Buffer.from(bytes.subarray(0, 4096)).toString(\"base64\").slice(0, 24)}\"`,\n },\n });\n });\n\n return Layer.effect(\n BundleResolver,\n Effect.succeed(\n BundleResolver.of({\n lookup: (input) => {\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n return respond(url);\n },\n }),\n ),\n );\n }\n}\n\nexport interface BundleFetchHandle {\n readonly uninstall: () => Promise<void>;\n}\n\nexport interface BundleFetchOptions {\n /** Own-namespace FS resolution — requires a staged `BOS_BUNDLE_DIR`. */\n namespace?: BundleNamespace;\n /**\n * Foreign-namespace stale-if-error cache (child tier): `/bundles/…` URLs\n * outside the own namespace write through to disk on success and serve\n * the last-known-good bytes when the origin fails. Enabled implicitly\n * when `namespace` is present (deployment contexts), or explicitly via\n * `BOS_BUNDLE_CACHE_DIR`.\n */\n cacheDir?: string;\n}\n\ntype FetchImpl = typeof globalThis.fetch;\n\nconst staleResponse = (bytes: Uint8Array, url: string): Response => {\n const name = url.split(\"?\")[0].split(\"/\").pop() ?? \"\";\n const contentType = MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(bytes.slice().buffer, {\n status: 200,\n headers: { \"content-type\": contentType, \"x-bundle-cache\": \"stale\" },\n });\n};\n\n/**\n * Installs the resolver as a global fetch interceptor — the one seam every\n * boot-time consumer shares (config manifest discovery, contract-type\n * fetches, orchestrator host loading, MF remoteEntry/identity probes).\n * One layered resolution policy: staged disk → network → stale-if-error.\n * Own-namespace URLs resolve from the staged directory and fall through to\n * the original fetch on a miss (a partially-staged namespace boots over the\n * wire instead of failing); a miss never touches the write-through cache —\n * for the own namespace the staged disk IS the resilience artifact. Foreign\n * `/bundles/…` URLs get the network with the stale-if-error cache; every\n * other URL falls through to the original fetch untouched. Inert when\n * neither a staged namespace nor a cache is configured.\n */\nexport function installGlobalBundleFetch(options: BundleFetchOptions): BundleFetchHandle {\n const namespace = options.namespace;\n const cacheDir = options.cacheDir;\n const runtime = namespace ? ManagedRuntime.make(BundleResolver.layer(namespace)) : null;\n const original: FetchImpl = globalThis.fetch;\n const patched = (async (input: Parameters<FetchImpl>[0], init?: Parameters<FetchImpl>[1]) => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n if (namespace && bundleUrlToLocalPath(url, namespace)) {\n if (!runtime) throw new Error(\"unreachable: resolver runtime missing\");\n const response = await runtime.runPromise(\n Effect.gen(function* () {\n const resolver = yield* BundleResolver;\n return yield* resolver.lookup(input);\n }),\n );\n if (response !== null) return response;\n return original(input, init);\n }\n\n if (cacheDir !== undefined && bundleCachePath(url, { cacheDir })) {\n return (async () => {\n try {\n const response = await original(input, init);\n if (response.ok) {\n try {\n const bytes = new Uint8Array(await response.clone().arrayBuffer());\n await writeBundleCache(url, bytes, { cacheDir });\n } catch {\n // write-through is best-effort\n }\n } else {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n }\n return response;\n } catch (error) {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n throw error;\n }\n })();\n }\n\n return original(input, init);\n }) as FetchImpl;\n const originalPreconnect = (original as Partial<FetchImpl>).preconnect;\n if (originalPreconnect) {\n patched.preconnect = originalPreconnect.bind(original);\n }\n globalThis.fetch = patched;\n return {\n uninstall: async () => {\n if (globalThis.fetch === patched) {\n globalThis.fetch = original;\n }\n if (runtime) await runtime.dispose();\n },\n };\n}\n\n/**\n * Boot-time installer for the CLI: derives the own namespace from\n * `BOS_BUNDLE_DIR` plus the runtime identity (`BOS_ACCOUNT`/`BOS_GATEWAY`\n * registry env taking precedence over the bos.config.json fields), and the\n * foreign-namespace cache from `BOS_BUNDLE_CACHE_DIR`. Tier auto-detection\n * (ADR 0021): an identity whose namespace is not actually staged under the\n * bundle dir drops to the registry tier — plain network fetch — instead of\n * deterministically 404ing own-namespace URLs against a foreign image's\n * staged bytes. Never throws — a missing or malformed identity leaves the\n * default network fetch in place.\n */\nexport function installBundleFetchFromEnv(input: {\n configPath?: string | null;\n}): BundleFetchHandle | null {\n const bundleDir = process.env.BOS_BUNDLE_DIR;\n const cacheDir = process.env.BOS_BUNDLE_CACHE_DIR;\n if (!bundleDir && cacheDir === undefined) return null;\n\n let configAccount: string | undefined;\n let configGateway: string | undefined;\n const configPath = input.configPath;\n if (configPath && !isAppDescriptorPath(configPath)) {\n try {\n const parsed = JSON.parse(readFileSync(configPath, \"utf8\")) as {\n account?: string;\n domain?: string;\n };\n configAccount = parsed.account;\n configGateway = parsed.domain;\n } catch {\n // unreadable config — identity may still come from the registry env\n }\n }\n\n const account = process.env.BOS_ACCOUNT ?? configAccount;\n const gateway = process.env.BOS_GATEWAY ?? configGateway;\n if (bundleDir && (!account || !gateway)) return null;\n\n let namespace: BundleNamespace | undefined;\n if (bundleDir && account && gateway) {\n if (existsSync(path.join(bundleDir, account, gateway))) {\n namespace = { bundleDir, account, gateway };\n } else {\n console.warn(\n `[bundles] staged namespace ${account}/${gateway} not found under ${bundleDir} — registry tier (network fetch)`,\n );\n }\n }\n return installGlobalBundleFetch({\n namespace,\n cacheDir: cacheDir ?? (namespace ? bundleCacheRoot() : undefined),\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bundle-fs-resolve.cjs","names":["Schema","bundleUrlToStagedPath","path","Context","Effect","readFile","cacheControlOf","Layer","ManagedRuntime","bundleCachePath","writeBundleCache","readBundleCache","isAppDescriptorPath","readFileSync","existsSync","bundleCacheRoot"],"sources":["../src/bundle-fs-resolve.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Context, Effect, Layer, ManagedRuntime, Schema } from \"effect\";\nimport { cacheControlOf } from \"every-plugin/build/artifact-names\";\nimport {\n bundleCachePath,\n bundleCacheRoot,\n readBundleCache,\n writeBundleCache,\n} from \"./bundle-cache\";\nimport { bundleUrlToStagedPath } from \"./bundle-path\";\nimport { isAppDescriptorPath } from \"./config\";\n\n/**\n * Local-first bundle resolution (ADR 0011 amendment; unified policy, ADR 0021\n * amendment): a self-contained runtime image stages its own namespace under\n * `BOS_BUNDLE_DIR` as `bundles/<account>/<gateway>/<workspace>/…` and consumes\n * those bytes directly from disk; a miss falls through to the network, so a\n * partially-staged namespace boots over the wire instead of failing. The\n * registry tier (children, `BOS_BUNDLE_DIR` unset) keeps the network fetch —\n * the namespace guard below only ever matches the runtime's own\n * account/gateway, never another runtime's layout.\n */\n\nexport interface BundleNamespace {\n readonly bundleDir: string;\n readonly account: string;\n readonly gateway: string;\n}\n\nexport class BundleReadError extends Schema.TaggedError<BundleReadError>()(\"BundleReadError\", {\n path: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n}) {}\n\nconst MIME_TYPES: Record<string, string> = {\n \".js\": \"text/javascript\",\n \".mjs\": \"text/javascript\",\n \".cjs\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".map\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".ico\": \"image/x-icon\",\n \".html\": \"text/html\",\n \".htm\": \"text/html\",\n \".webmanifest\": \"application/manifest+json\",\n \".md\": \"text/markdown\",\n \".ts\": \"text/plain\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".otf\": \"font/otf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".wasm\": \"application/wasm\",\n \".txt\": \"text/plain\",\n \".xml\": \"application/xml\",\n};\n\n/**\n * Own-namespace path for a bundle URL: the shared URL→staged-path mapping,\n * narrowed to the runtime identity — a URL whose encoded namespace differs\n * from `namespace.account`/`namespace.gateway` must fall to the network/cache\n * path, never serve a sibling namespace's staged bytes.\n */\nexport function bundleUrlToLocalPath(url: string, namespace: BundleNamespace): string | null {\n const filePath = bundleUrlToStagedPath(url, namespace.bundleDir);\n if (!filePath) return null;\n\n const nsDir = path.resolve(namespace.bundleDir, namespace.account, namespace.gateway);\n if (filePath !== nsDir && !filePath.startsWith(nsDir + path.sep)) return null;\n\n return filePath;\n}\n\nexport class BundleResolver extends Context.Service<\n BundleResolver,\n {\n /** Resolve a fetch target from the staged namespace; null = miss or not our namespace — fall through to the network. */\n readonly lookup: (input: string | URL | Request) => Effect.Effect<Response | null>;\n }\n>()(\"everything-dev/bundle-fs-resolve/BundleResolver\") {\n static layer(namespace: BundleNamespace): Layer.Layer<BundleResolver> {\n const respond = Effect.fn(\"BundleResolver.respond\")(function* (\n url: string,\n ): Effect.fn.Return<Response | null, never> {\n const filePath = bundleUrlToLocalPath(url, namespace);\n if (!filePath) return null;\n const bytes = yield* Effect.tryPromise({\n try: () => readFile(filePath),\n catch: (cause) => new BundleReadError({ path: filePath, cause }),\n }).pipe(Effect.catchTag(\"BundleReadError\", () => Effect.succeed(null)));\n if (bytes === null) return null;\n const name = path.basename(filePath);\n const contentType =\n MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(new Uint8Array(bytes), {\n headers: {\n \"content-type\": contentType,\n \"cache-control\": cacheControlOf(name),\n etag: `\"${Buffer.from(bytes.subarray(0, 4096)).toString(\"base64\").slice(0, 24)}\"`,\n },\n });\n });\n\n return Layer.effect(\n BundleResolver,\n Effect.succeed(\n BundleResolver.of({\n lookup: (input) => {\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n return respond(url);\n },\n }),\n ),\n );\n }\n}\n\nexport interface BundleFetchHandle {\n readonly uninstall: () => Promise<void>;\n}\n\nexport interface BundleFetchOptions {\n /** Own-namespace FS resolution — requires a staged `BOS_BUNDLE_DIR`. */\n namespace?: BundleNamespace;\n /**\n * Foreign-namespace stale-if-error cache (child tier): `/bundles/…` URLs\n * outside the own namespace write through to disk on success and serve\n * the last-known-good bytes when the origin fails. Enabled implicitly\n * when `namespace` is present (deployment contexts), or explicitly via\n * `BOS_BUNDLE_CACHE_DIR`.\n */\n cacheDir?: string;\n}\n\ntype FetchImpl = typeof globalThis.fetch;\n\nconst staleResponse = (bytes: Uint8Array, url: string): Response => {\n const name = url.split(\"?\")[0].split(\"/\").pop() ?? \"\";\n const contentType = MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(bytes.slice().buffer, {\n status: 200,\n headers: { \"content-type\": contentType, \"x-bundle-cache\": \"stale\" },\n });\n};\n\nconst FETCH_RETRY_ATTEMPTS = 3;\nconst FETCH_RETRY_BASE_DELAY_MS = 250;\n\nconst isTransientStatus = (status: number): boolean => status === 429 || status >= 500;\n\nfunction requestMethod(input: Parameters<FetchImpl>[0], init?: Parameters<FetchImpl>[1]): string {\n if (init?.method) return init.method.toUpperCase();\n if (typeof input === \"string\" || input instanceof URL) return \"GET\";\n return input.method.toUpperCase();\n}\n\n/**\n * Bounded transient-failure retry for idempotent GETs crossing the\n * interceptor (ADR 0021 amendment — the CDN-only image): a cold boot has no\n * staged fallback, so one edge/registry hiccup during the load window would\n * hard-fail the host or a plugin with no recourse. Network errors and\n * 429/5xx responses retry twice (250ms, 750ms backoff) before propagating;\n * non-transient statuses and non-GET requests pass through untouched. After\n * exhausting attempts a stale-if-error cache still sees the final failure.\n */\nasync function fetchWithTransientRetry(\n original: FetchImpl,\n input: Parameters<FetchImpl>[0],\n init?: Parameters<FetchImpl>[1],\n): Promise<Response> {\n if (requestMethod(input, init) !== \"GET\") return original(input, init);\n let lastError: unknown;\n let lastResponse: Response | null = null;\n for (let attempt = 0; attempt < FETCH_RETRY_ATTEMPTS; attempt++) {\n if (attempt > 0) {\n await new Promise((resolve) =>\n setTimeout(resolve, FETCH_RETRY_BASE_DELAY_MS * 3 ** (attempt - 1)),\n );\n }\n try {\n const response = await original(input, init);\n if (!isTransientStatus(response.status)) return response;\n lastResponse = response;\n } catch (error) {\n lastError = error;\n }\n }\n if (lastResponse) return lastResponse;\n throw lastError;\n}\n\n/**\n * Installs the resolver as a global fetch interceptor — the one seam every\n * boot-time consumer shares (config manifest discovery, contract-type\n * fetches, orchestrator host loading, MF remoteEntry/identity probes).\n * One layered resolution policy: staged disk → network → stale-if-error.\n * Own-namespace URLs resolve from the staged directory and fall through to\n * the original fetch on a miss (a partially-staged namespace boots over the\n * wire instead of failing); a miss never touches the write-through cache —\n * for the own namespace the staged disk IS the resilience artifact. Foreign\n * `/bundles/…` URLs get the network with the stale-if-error cache; every\n * other URL falls through to the original fetch untouched. Inert when\n * neither a staged namespace nor a cache is configured.\n */\nexport function installGlobalBundleFetch(options: BundleFetchOptions): BundleFetchHandle {\n const namespace = options.namespace;\n const cacheDir = options.cacheDir;\n const runtime = namespace ? ManagedRuntime.make(BundleResolver.layer(namespace)) : null;\n const original: FetchImpl = globalThis.fetch;\n const patched = (async (input: Parameters<FetchImpl>[0], init?: Parameters<FetchImpl>[1]) => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n if (namespace && bundleUrlToLocalPath(url, namespace)) {\n if (!runtime) throw new Error(\"unreachable: resolver runtime missing\");\n const response = await runtime.runPromise(\n Effect.gen(function* () {\n const resolver = yield* BundleResolver;\n return yield* resolver.lookup(input);\n }),\n );\n if (response !== null) return response;\n return fetchWithTransientRetry(original, input, init);\n }\n\n if (cacheDir !== undefined && bundleCachePath(url, { cacheDir })) {\n return (async () => {\n try {\n const response = await fetchWithTransientRetry(original, input, init);\n if (response.ok) {\n try {\n const bytes = new Uint8Array(await response.clone().arrayBuffer());\n await writeBundleCache(url, bytes, { cacheDir });\n } catch {\n // write-through is best-effort\n }\n } else {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n }\n return response;\n } catch (error) {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n throw error;\n }\n })();\n }\n\n return fetchWithTransientRetry(original, input, init);\n }) as FetchImpl;\n const originalPreconnect = (original as Partial<FetchImpl>).preconnect;\n if (originalPreconnect) {\n patched.preconnect = originalPreconnect.bind(original);\n }\n globalThis.fetch = patched;\n return {\n uninstall: async () => {\n if (globalThis.fetch === patched) {\n globalThis.fetch = original;\n }\n if (runtime) await runtime.dispose();\n },\n };\n}\n\n/**\n * Boot-time installer for the CLI: derives the own namespace from\n * `BOS_BUNDLE_DIR` plus the runtime identity (`BOS_ACCOUNT`/`BOS_GATEWAY`\n * registry env taking precedence over the bos.config.json fields), and the\n * foreign-namespace cache from `BOS_BUNDLE_CACHE_DIR`. Tier auto-detection\n * (ADR 0021): an identity whose namespace is not actually staged under the\n * bundle dir drops to the registry tier — plain network fetch — instead of\n * deterministically 404ing own-namespace URLs against a foreign image's\n * staged bytes. Never throws — a missing or malformed identity leaves the\n * default network fetch in place.\n */\nexport function installBundleFetchFromEnv(input: {\n configPath?: string | null;\n}): BundleFetchHandle | null {\n const bundleDir = process.env.BOS_BUNDLE_DIR;\n const cacheDir = process.env.BOS_BUNDLE_CACHE_DIR;\n if (!bundleDir && cacheDir === undefined) return null;\n\n let configAccount: string | undefined;\n let configGateway: string | undefined;\n const configPath = input.configPath;\n if (configPath && !isAppDescriptorPath(configPath)) {\n try {\n const parsed = JSON.parse(readFileSync(configPath, \"utf8\")) as {\n account?: string;\n domain?: string;\n };\n configAccount = parsed.account;\n configGateway = parsed.domain;\n } catch {\n // unreadable config — identity may still come from the registry env\n }\n }\n\n const account = process.env.BOS_ACCOUNT ?? configAccount;\n const gateway = process.env.BOS_GATEWAY ?? configGateway;\n if (bundleDir && (!account || !gateway)) return null;\n\n let namespace: BundleNamespace | undefined;\n if (bundleDir && account && gateway) {\n if (existsSync(path.join(bundleDir, account, gateway))) {\n namespace = { bundleDir, account, gateway };\n } else {\n console.warn(\n `[bundles] staged namespace ${account}/${gateway} not found under ${bundleDir} — registry tier (network fetch)`,\n );\n }\n }\n return installGlobalBundleFetch({\n namespace,\n cacheDir: cacheDir ?? (namespace ? bundleCacheRoot() : undefined),\n });\n}\n"],"mappings":"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"}
@@ -92,6 +92,40 @@ const staleResponse = (bytes, url) => {
92
92
  }
93
93
  });
94
94
  };
95
+ const FETCH_RETRY_ATTEMPTS = 3;
96
+ const FETCH_RETRY_BASE_DELAY_MS = 250;
97
+ const isTransientStatus = (status) => status === 429 || status >= 500;
98
+ function requestMethod(input, init) {
99
+ if (init?.method) return init.method.toUpperCase();
100
+ if (typeof input === "string" || input instanceof URL) return "GET";
101
+ return input.method.toUpperCase();
102
+ }
103
+ /**
104
+ * Bounded transient-failure retry for idempotent GETs crossing the
105
+ * interceptor (ADR 0021 amendment — the CDN-only image): a cold boot has no
106
+ * staged fallback, so one edge/registry hiccup during the load window would
107
+ * hard-fail the host or a plugin with no recourse. Network errors and
108
+ * 429/5xx responses retry twice (250ms, 750ms backoff) before propagating;
109
+ * non-transient statuses and non-GET requests pass through untouched. After
110
+ * exhausting attempts a stale-if-error cache still sees the final failure.
111
+ */
112
+ async function fetchWithTransientRetry(original, input, init) {
113
+ if (requestMethod(input, init) !== "GET") return original(input, init);
114
+ let lastError;
115
+ let lastResponse = null;
116
+ for (let attempt = 0; attempt < FETCH_RETRY_ATTEMPTS; attempt++) {
117
+ if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, FETCH_RETRY_BASE_DELAY_MS * 3 ** (attempt - 1)));
118
+ try {
119
+ const response = await original(input, init);
120
+ if (!isTransientStatus(response.status)) return response;
121
+ lastResponse = response;
122
+ } catch (error) {
123
+ lastError = error;
124
+ }
125
+ }
126
+ if (lastResponse) return lastResponse;
127
+ throw lastError;
128
+ }
95
129
  /**
96
130
  * Installs the resolver as a global fetch interceptor — the one seam every
97
131
  * boot-time consumer shares (config manifest discovery, contract-type
@@ -118,11 +152,11 @@ function installGlobalBundleFetch(options) {
118
152
  return yield* (yield* BundleResolver).lookup(input);
119
153
  }));
120
154
  if (response !== null) return response;
121
- return original(input, init);
155
+ return fetchWithTransientRetry(original, input, init);
122
156
  }
123
157
  if (cacheDir !== void 0 && bundleCachePath(url, { cacheDir })) return (async () => {
124
158
  try {
125
- const response = await original(input, init);
159
+ const response = await fetchWithTransientRetry(original, input, init);
126
160
  if (response.ok) try {
127
161
  const bytes = new Uint8Array(await response.clone().arrayBuffer());
128
162
  await writeBundleCache(url, bytes, { cacheDir });
@@ -138,7 +172,7 @@ function installGlobalBundleFetch(options) {
138
172
  throw error;
139
173
  }
140
174
  })();
141
- return original(input, init);
175
+ return fetchWithTransientRetry(original, input, init);
142
176
  });
143
177
  const originalPreconnect = original.preconnect;
144
178
  if (originalPreconnect) patched.preconnect = originalPreconnect.bind(original);
@@ -1 +1 @@
1
- {"version":3,"file":"bundle-fs-resolve.mjs","names":[],"sources":["../src/bundle-fs-resolve.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Context, Effect, Layer, ManagedRuntime, Schema } from \"effect\";\nimport { cacheControlOf } from \"every-plugin/build/artifact-names\";\nimport {\n bundleCachePath,\n bundleCacheRoot,\n readBundleCache,\n writeBundleCache,\n} from \"./bundle-cache\";\nimport { bundleUrlToStagedPath } from \"./bundle-path\";\nimport { isAppDescriptorPath } from \"./config\";\n\n/**\n * Local-first bundle resolution (ADR 0011 amendment; unified policy, ADR 0021\n * amendment): a self-contained runtime image stages its own namespace under\n * `BOS_BUNDLE_DIR` as `bundles/<account>/<gateway>/<workspace>/…` and consumes\n * those bytes directly from disk; a miss falls through to the network, so a\n * partially-staged namespace boots over the wire instead of failing. The\n * registry tier (children, `BOS_BUNDLE_DIR` unset) keeps the network fetch —\n * the namespace guard below only ever matches the runtime's own\n * account/gateway, never another runtime's layout.\n */\n\nexport interface BundleNamespace {\n readonly bundleDir: string;\n readonly account: string;\n readonly gateway: string;\n}\n\nexport class BundleReadError extends Schema.TaggedError<BundleReadError>()(\"BundleReadError\", {\n path: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n}) {}\n\nconst MIME_TYPES: Record<string, string> = {\n \".js\": \"text/javascript\",\n \".mjs\": \"text/javascript\",\n \".cjs\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".map\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".ico\": \"image/x-icon\",\n \".html\": \"text/html\",\n \".htm\": \"text/html\",\n \".webmanifest\": \"application/manifest+json\",\n \".md\": \"text/markdown\",\n \".ts\": \"text/plain\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".otf\": \"font/otf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".wasm\": \"application/wasm\",\n \".txt\": \"text/plain\",\n \".xml\": \"application/xml\",\n};\n\n/**\n * Own-namespace path for a bundle URL: the shared URL→staged-path mapping,\n * narrowed to the runtime identity — a URL whose encoded namespace differs\n * from `namespace.account`/`namespace.gateway` must fall to the network/cache\n * path, never serve a sibling namespace's staged bytes.\n */\nexport function bundleUrlToLocalPath(url: string, namespace: BundleNamespace): string | null {\n const filePath = bundleUrlToStagedPath(url, namespace.bundleDir);\n if (!filePath) return null;\n\n const nsDir = path.resolve(namespace.bundleDir, namespace.account, namespace.gateway);\n if (filePath !== nsDir && !filePath.startsWith(nsDir + path.sep)) return null;\n\n return filePath;\n}\n\nexport class BundleResolver extends Context.Service<\n BundleResolver,\n {\n /** Resolve a fetch target from the staged namespace; null = miss or not our namespace — fall through to the network. */\n readonly lookup: (input: string | URL | Request) => Effect.Effect<Response | null>;\n }\n>()(\"everything-dev/bundle-fs-resolve/BundleResolver\") {\n static layer(namespace: BundleNamespace): Layer.Layer<BundleResolver> {\n const respond = Effect.fn(\"BundleResolver.respond\")(function* (\n url: string,\n ): Effect.fn.Return<Response | null, never> {\n const filePath = bundleUrlToLocalPath(url, namespace);\n if (!filePath) return null;\n const bytes = yield* Effect.tryPromise({\n try: () => readFile(filePath),\n catch: (cause) => new BundleReadError({ path: filePath, cause }),\n }).pipe(Effect.catchTag(\"BundleReadError\", () => Effect.succeed(null)));\n if (bytes === null) return null;\n const name = path.basename(filePath);\n const contentType =\n MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(new Uint8Array(bytes), {\n headers: {\n \"content-type\": contentType,\n \"cache-control\": cacheControlOf(name),\n etag: `\"${Buffer.from(bytes.subarray(0, 4096)).toString(\"base64\").slice(0, 24)}\"`,\n },\n });\n });\n\n return Layer.effect(\n BundleResolver,\n Effect.succeed(\n BundleResolver.of({\n lookup: (input) => {\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n return respond(url);\n },\n }),\n ),\n );\n }\n}\n\nexport interface BundleFetchHandle {\n readonly uninstall: () => Promise<void>;\n}\n\nexport interface BundleFetchOptions {\n /** Own-namespace FS resolution — requires a staged `BOS_BUNDLE_DIR`. */\n namespace?: BundleNamespace;\n /**\n * Foreign-namespace stale-if-error cache (child tier): `/bundles/…` URLs\n * outside the own namespace write through to disk on success and serve\n * the last-known-good bytes when the origin fails. Enabled implicitly\n * when `namespace` is present (deployment contexts), or explicitly via\n * `BOS_BUNDLE_CACHE_DIR`.\n */\n cacheDir?: string;\n}\n\ntype FetchImpl = typeof globalThis.fetch;\n\nconst staleResponse = (bytes: Uint8Array, url: string): Response => {\n const name = url.split(\"?\")[0].split(\"/\").pop() ?? \"\";\n const contentType = MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(bytes.slice().buffer, {\n status: 200,\n headers: { \"content-type\": contentType, \"x-bundle-cache\": \"stale\" },\n });\n};\n\n/**\n * Installs the resolver as a global fetch interceptor — the one seam every\n * boot-time consumer shares (config manifest discovery, contract-type\n * fetches, orchestrator host loading, MF remoteEntry/identity probes).\n * One layered resolution policy: staged disk → network → stale-if-error.\n * Own-namespace URLs resolve from the staged directory and fall through to\n * the original fetch on a miss (a partially-staged namespace boots over the\n * wire instead of failing); a miss never touches the write-through cache —\n * for the own namespace the staged disk IS the resilience artifact. Foreign\n * `/bundles/…` URLs get the network with the stale-if-error cache; every\n * other URL falls through to the original fetch untouched. Inert when\n * neither a staged namespace nor a cache is configured.\n */\nexport function installGlobalBundleFetch(options: BundleFetchOptions): BundleFetchHandle {\n const namespace = options.namespace;\n const cacheDir = options.cacheDir;\n const runtime = namespace ? ManagedRuntime.make(BundleResolver.layer(namespace)) : null;\n const original: FetchImpl = globalThis.fetch;\n const patched = (async (input: Parameters<FetchImpl>[0], init?: Parameters<FetchImpl>[1]) => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n if (namespace && bundleUrlToLocalPath(url, namespace)) {\n if (!runtime) throw new Error(\"unreachable: resolver runtime missing\");\n const response = await runtime.runPromise(\n Effect.gen(function* () {\n const resolver = yield* BundleResolver;\n return yield* resolver.lookup(input);\n }),\n );\n if (response !== null) return response;\n return original(input, init);\n }\n\n if (cacheDir !== undefined && bundleCachePath(url, { cacheDir })) {\n return (async () => {\n try {\n const response = await original(input, init);\n if (response.ok) {\n try {\n const bytes = new Uint8Array(await response.clone().arrayBuffer());\n await writeBundleCache(url, bytes, { cacheDir });\n } catch {\n // write-through is best-effort\n }\n } else {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n }\n return response;\n } catch (error) {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n throw error;\n }\n })();\n }\n\n return original(input, init);\n }) as FetchImpl;\n const originalPreconnect = (original as Partial<FetchImpl>).preconnect;\n if (originalPreconnect) {\n patched.preconnect = originalPreconnect.bind(original);\n }\n globalThis.fetch = patched;\n return {\n uninstall: async () => {\n if (globalThis.fetch === patched) {\n globalThis.fetch = original;\n }\n if (runtime) await runtime.dispose();\n },\n };\n}\n\n/**\n * Boot-time installer for the CLI: derives the own namespace from\n * `BOS_BUNDLE_DIR` plus the runtime identity (`BOS_ACCOUNT`/`BOS_GATEWAY`\n * registry env taking precedence over the bos.config.json fields), and the\n * foreign-namespace cache from `BOS_BUNDLE_CACHE_DIR`. Tier auto-detection\n * (ADR 0021): an identity whose namespace is not actually staged under the\n * bundle dir drops to the registry tier — plain network fetch — instead of\n * deterministically 404ing own-namespace URLs against a foreign image's\n * staged bytes. Never throws — a missing or malformed identity leaves the\n * default network fetch in place.\n */\nexport function installBundleFetchFromEnv(input: {\n configPath?: string | null;\n}): BundleFetchHandle | null {\n const bundleDir = process.env.BOS_BUNDLE_DIR;\n const cacheDir = process.env.BOS_BUNDLE_CACHE_DIR;\n if (!bundleDir && cacheDir === undefined) return null;\n\n let configAccount: string | undefined;\n let configGateway: string | undefined;\n const configPath = input.configPath;\n if (configPath && !isAppDescriptorPath(configPath)) {\n try {\n const parsed = JSON.parse(readFileSync(configPath, \"utf8\")) as {\n account?: string;\n domain?: string;\n };\n configAccount = parsed.account;\n configGateway = parsed.domain;\n } catch {\n // unreadable config — identity may still come from the registry env\n }\n }\n\n const account = process.env.BOS_ACCOUNT ?? configAccount;\n const gateway = process.env.BOS_GATEWAY ?? configGateway;\n if (bundleDir && (!account || !gateway)) return null;\n\n let namespace: BundleNamespace | undefined;\n if (bundleDir && account && gateway) {\n if (existsSync(path.join(bundleDir, account, gateway))) {\n namespace = { bundleDir, account, gateway };\n } else {\n console.warn(\n `[bundles] staged namespace ${account}/${gateway} not found under ${bundleDir} — registry tier (network fetch)`,\n );\n }\n }\n return installGlobalBundleFetch({\n namespace,\n cacheDir: cacheDir ?? (namespace ? bundleCacheRoot() : undefined),\n });\n}\n"],"mappings":";;;;;;;;;;AA+BA,IAAa,kBAAb,cAAqC,OAAO,YAA6B,CAAC,CAAC,mBAAmB;CAC5F,MAAM,OAAO;CACb,OAAO,OAAO,SAAS,OAAO,OAAO;AACvC,CAAC,CAAC,CAAC,CAAC;AAEJ,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,gBAAgB;CAChB,OAAO;CACP,OAAO;CACP,SAAS;CACT,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ;AACV;;;;;;;AAQA,SAAgB,qBAAqB,KAAa,WAA2C;CAC3F,MAAM,WAAW,sBAAsB,KAAK,UAAU,SAAS;CAC/D,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,QAAQ,KAAK,QAAQ,UAAU,WAAW,UAAU,SAAS,UAAU,OAAO;CACpF,IAAI,aAAa,SAAS,CAAC,SAAS,WAAW,QAAQ,KAAK,GAAG,GAAG,OAAO;CAEzE,OAAO;AACT;AAEA,IAAa,iBAAb,MAAa,uBAAuB,QAAQ,QAM1C,CAAC,CAAC,iDAAiD,CAAC,CAAC;CACrD,OAAO,MAAM,WAAyD;EACpE,MAAM,UAAU,OAAO,GAAG,wBAAwB,CAAC,CAAC,WAClD,KAC0C;GAC1C,MAAM,WAAW,qBAAqB,KAAK,SAAS;GACpD,IAAI,CAAC,UAAU,OAAO;GACtB,MAAM,QAAQ,OAAO,OAAO,WAAW;IACrC,WAAW,SAAS,QAAQ;IAC5B,QAAQ,UAAU,IAAI,gBAAgB;KAAE,MAAM;KAAU;IAAM,CAAC;GACjE,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,yBAAyB,OAAO,QAAQ,IAAI,CAAC,CAAC;GACtE,IAAI,UAAU,MAAM,OAAO;GAC3B,MAAM,OAAO,KAAK,SAAS,QAAQ;GACnC,MAAM,cACJ,WAAW,KAAK,QAAQ,IAAI,CAAC,CAAC,YAAY,MAAM;GAClD,OAAO,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,EACzC,SAAS;IACP,gBAAgB;IAChB,iBAAiB,eAAe,IAAI;IACpC,MAAM,IAAI,OAAO,KAAK,MAAM,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,MAAM,GAAG,EAAE,EAAE;GACjF,EACF,CAAC;EACH,CAAC;EAED,OAAO,MAAM,OACX,gBACA,OAAO,QACL,eAAe,GAAG,EAChB,SAAS,UAAU;GACjB,MAAM,MACJ,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAO,MAAM;GAChF,OAAO,QAAQ,GAAG;EACpB,EACF,CAAC,CACH,CACF;CACF;AACF;AAqBA,MAAM,iBAAiB,OAAmB,QAA0B;CAClE,MAAM,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CACnD,MAAM,cAAc,WAAW,KAAK,QAAQ,IAAI,CAAC,CAAC,YAAY,MAAM;CACpE,OAAO,IAAI,SAAS,MAAM,MAAM,CAAC,CAAC,QAAQ;EACxC,QAAQ;EACR,SAAS;GAAE,gBAAgB;GAAa,kBAAkB;EAAQ;CACpE,CAAC;AACH;;;;;;;;;;;;;;AAeA,SAAgB,yBAAyB,SAAgD;CACvF,MAAM,YAAY,QAAQ;CAC1B,MAAM,WAAW,QAAQ;CACzB,MAAM,UAAU,YAAY,eAAe,KAAK,eAAe,MAAM,SAAS,CAAC,IAAI;CACnF,MAAM,WAAsB,WAAW;CACvC,MAAM,WAAW,OAAO,OAAiC,SAAoC;EAC3F,MAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAO,MAAM;EAE1F,IAAI,aAAa,qBAAqB,KAAK,SAAS,GAAG;GACrD,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,uCAAuC;GACrE,MAAM,WAAW,MAAM,QAAQ,WAC7B,OAAO,IAAI,aAAa;IAEtB,OAAO,QAAO,OADU,eACF,CAAC,OAAO,KAAK;GACrC,CAAC,CACH;GACA,IAAI,aAAa,MAAM,OAAO;GAC9B,OAAO,SAAS,OAAO,IAAI;EAC7B;EAEA,IAAI,aAAa,UAAa,gBAAgB,KAAK,EAAE,SAAS,CAAC,GAC7D,QAAQ,YAAY;GAClB,IAAI;IACF,MAAM,WAAW,MAAM,SAAS,OAAO,IAAI;IAC3C,IAAI,SAAS,IACX,IAAI;KACF,MAAM,QAAQ,IAAI,WAAW,MAAM,SAAS,MAAM,CAAC,CAAC,YAAY,CAAC;KACjE,MAAM,iBAAiB,KAAK,OAAO,EAAE,SAAS,CAAC;IACjD,QAAQ,CAER;SACK;KACL,MAAM,SAAS,MAAM,gBAAgB,KAAK,EAAE,SAAS,CAAC;KACtD,IAAI,QAAQ,OAAO,cAAc,QAAQ,GAAG;IAC9C;IACA,OAAO;GACT,SAAS,OAAO;IACd,MAAM,SAAS,MAAM,gBAAgB,KAAK,EAAE,SAAS,CAAC;IACtD,IAAI,QAAQ,OAAO,cAAc,QAAQ,GAAG;IAC5C,MAAM;GACR;EACF,EAAC,CAAE;EAGL,OAAO,SAAS,OAAO,IAAI;CAC7B;CACA,MAAM,qBAAsB,SAAgC;CAC5D,IAAI,oBACF,QAAQ,aAAa,mBAAmB,KAAK,QAAQ;CAEvD,WAAW,QAAQ;CACnB,OAAO,EACL,WAAW,YAAY;EACrB,IAAI,WAAW,UAAU,SACvB,WAAW,QAAQ;EAErB,IAAI,SAAS,MAAM,QAAQ,QAAQ;CACrC,EACF;AACF;;;;;;;;;;;;AAaA,SAAgB,0BAA0B,OAEb;CAC3B,MAAM,YAAY,QAAQ,IAAI;CAC9B,MAAM,WAAW,QAAQ,IAAI;CAC7B,IAAI,CAAC,aAAa,aAAa,QAAW,OAAO;CAEjD,IAAI;CACJ,IAAI;CACJ,MAAM,aAAa,MAAM;CACzB,IAAI,cAAc,CAAC,oBAAoB,UAAU,GAC/C,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,aAAa,YAAY,MAAM,CAAC;EAI1D,gBAAgB,OAAO;EACvB,gBAAgB,OAAO;CACzB,QAAQ,CAER;CAGF,MAAM,UAAU,QAAQ,IAAI,eAAe;CAC3C,MAAM,UAAU,QAAQ,IAAI,eAAe;CAC3C,IAAI,cAAc,CAAC,WAAW,CAAC,UAAU,OAAO;CAEhD,IAAI;CACJ,IAAI,aAAa,WAAW,SAAS;EACnC,IAAI,WAAW,KAAK,KAAK,WAAW,SAAS,OAAO,CAAC,GACnD,YAAY;GAAE;GAAW;GAAS;EAAQ;OAE1C,QAAQ,KACN,8BAA8B,QAAQ,GAAG,QAAQ,mBAAmB,UAAU,iCAChF;CAEJ;CACA,OAAO,yBAAyB;EAC9B;EACA,UAAU,aAAa,YAAY,gBAAgB,IAAI;CACzD,CAAC;AACH"}
1
+ {"version":3,"file":"bundle-fs-resolve.mjs","names":[],"sources":["../src/bundle-fs-resolve.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Context, Effect, Layer, ManagedRuntime, Schema } from \"effect\";\nimport { cacheControlOf } from \"every-plugin/build/artifact-names\";\nimport {\n bundleCachePath,\n bundleCacheRoot,\n readBundleCache,\n writeBundleCache,\n} from \"./bundle-cache\";\nimport { bundleUrlToStagedPath } from \"./bundle-path\";\nimport { isAppDescriptorPath } from \"./config\";\n\n/**\n * Local-first bundle resolution (ADR 0011 amendment; unified policy, ADR 0021\n * amendment): a self-contained runtime image stages its own namespace under\n * `BOS_BUNDLE_DIR` as `bundles/<account>/<gateway>/<workspace>/…` and consumes\n * those bytes directly from disk; a miss falls through to the network, so a\n * partially-staged namespace boots over the wire instead of failing. The\n * registry tier (children, `BOS_BUNDLE_DIR` unset) keeps the network fetch —\n * the namespace guard below only ever matches the runtime's own\n * account/gateway, never another runtime's layout.\n */\n\nexport interface BundleNamespace {\n readonly bundleDir: string;\n readonly account: string;\n readonly gateway: string;\n}\n\nexport class BundleReadError extends Schema.TaggedError<BundleReadError>()(\"BundleReadError\", {\n path: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n}) {}\n\nconst MIME_TYPES: Record<string, string> = {\n \".js\": \"text/javascript\",\n \".mjs\": \"text/javascript\",\n \".cjs\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".map\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".ico\": \"image/x-icon\",\n \".html\": \"text/html\",\n \".htm\": \"text/html\",\n \".webmanifest\": \"application/manifest+json\",\n \".md\": \"text/markdown\",\n \".ts\": \"text/plain\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".otf\": \"font/otf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".wasm\": \"application/wasm\",\n \".txt\": \"text/plain\",\n \".xml\": \"application/xml\",\n};\n\n/**\n * Own-namespace path for a bundle URL: the shared URL→staged-path mapping,\n * narrowed to the runtime identity — a URL whose encoded namespace differs\n * from `namespace.account`/`namespace.gateway` must fall to the network/cache\n * path, never serve a sibling namespace's staged bytes.\n */\nexport function bundleUrlToLocalPath(url: string, namespace: BundleNamespace): string | null {\n const filePath = bundleUrlToStagedPath(url, namespace.bundleDir);\n if (!filePath) return null;\n\n const nsDir = path.resolve(namespace.bundleDir, namespace.account, namespace.gateway);\n if (filePath !== nsDir && !filePath.startsWith(nsDir + path.sep)) return null;\n\n return filePath;\n}\n\nexport class BundleResolver extends Context.Service<\n BundleResolver,\n {\n /** Resolve a fetch target from the staged namespace; null = miss or not our namespace — fall through to the network. */\n readonly lookup: (input: string | URL | Request) => Effect.Effect<Response | null>;\n }\n>()(\"everything-dev/bundle-fs-resolve/BundleResolver\") {\n static layer(namespace: BundleNamespace): Layer.Layer<BundleResolver> {\n const respond = Effect.fn(\"BundleResolver.respond\")(function* (\n url: string,\n ): Effect.fn.Return<Response | null, never> {\n const filePath = bundleUrlToLocalPath(url, namespace);\n if (!filePath) return null;\n const bytes = yield* Effect.tryPromise({\n try: () => readFile(filePath),\n catch: (cause) => new BundleReadError({ path: filePath, cause }),\n }).pipe(Effect.catchTag(\"BundleReadError\", () => Effect.succeed(null)));\n if (bytes === null) return null;\n const name = path.basename(filePath);\n const contentType =\n MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(new Uint8Array(bytes), {\n headers: {\n \"content-type\": contentType,\n \"cache-control\": cacheControlOf(name),\n etag: `\"${Buffer.from(bytes.subarray(0, 4096)).toString(\"base64\").slice(0, 24)}\"`,\n },\n });\n });\n\n return Layer.effect(\n BundleResolver,\n Effect.succeed(\n BundleResolver.of({\n lookup: (input) => {\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n return respond(url);\n },\n }),\n ),\n );\n }\n}\n\nexport interface BundleFetchHandle {\n readonly uninstall: () => Promise<void>;\n}\n\nexport interface BundleFetchOptions {\n /** Own-namespace FS resolution — requires a staged `BOS_BUNDLE_DIR`. */\n namespace?: BundleNamespace;\n /**\n * Foreign-namespace stale-if-error cache (child tier): `/bundles/…` URLs\n * outside the own namespace write through to disk on success and serve\n * the last-known-good bytes when the origin fails. Enabled implicitly\n * when `namespace` is present (deployment contexts), or explicitly via\n * `BOS_BUNDLE_CACHE_DIR`.\n */\n cacheDir?: string;\n}\n\ntype FetchImpl = typeof globalThis.fetch;\n\nconst staleResponse = (bytes: Uint8Array, url: string): Response => {\n const name = url.split(\"?\")[0].split(\"/\").pop() ?? \"\";\n const contentType = MIME_TYPES[path.extname(name).toLowerCase()] ?? \"application/octet-stream\";\n return new Response(bytes.slice().buffer, {\n status: 200,\n headers: { \"content-type\": contentType, \"x-bundle-cache\": \"stale\" },\n });\n};\n\nconst FETCH_RETRY_ATTEMPTS = 3;\nconst FETCH_RETRY_BASE_DELAY_MS = 250;\n\nconst isTransientStatus = (status: number): boolean => status === 429 || status >= 500;\n\nfunction requestMethod(input: Parameters<FetchImpl>[0], init?: Parameters<FetchImpl>[1]): string {\n if (init?.method) return init.method.toUpperCase();\n if (typeof input === \"string\" || input instanceof URL) return \"GET\";\n return input.method.toUpperCase();\n}\n\n/**\n * Bounded transient-failure retry for idempotent GETs crossing the\n * interceptor (ADR 0021 amendment — the CDN-only image): a cold boot has no\n * staged fallback, so one edge/registry hiccup during the load window would\n * hard-fail the host or a plugin with no recourse. Network errors and\n * 429/5xx responses retry twice (250ms, 750ms backoff) before propagating;\n * non-transient statuses and non-GET requests pass through untouched. After\n * exhausting attempts a stale-if-error cache still sees the final failure.\n */\nasync function fetchWithTransientRetry(\n original: FetchImpl,\n input: Parameters<FetchImpl>[0],\n init?: Parameters<FetchImpl>[1],\n): Promise<Response> {\n if (requestMethod(input, init) !== \"GET\") return original(input, init);\n let lastError: unknown;\n let lastResponse: Response | null = null;\n for (let attempt = 0; attempt < FETCH_RETRY_ATTEMPTS; attempt++) {\n if (attempt > 0) {\n await new Promise((resolve) =>\n setTimeout(resolve, FETCH_RETRY_BASE_DELAY_MS * 3 ** (attempt - 1)),\n );\n }\n try {\n const response = await original(input, init);\n if (!isTransientStatus(response.status)) return response;\n lastResponse = response;\n } catch (error) {\n lastError = error;\n }\n }\n if (lastResponse) return lastResponse;\n throw lastError;\n}\n\n/**\n * Installs the resolver as a global fetch interceptor — the one seam every\n * boot-time consumer shares (config manifest discovery, contract-type\n * fetches, orchestrator host loading, MF remoteEntry/identity probes).\n * One layered resolution policy: staged disk → network → stale-if-error.\n * Own-namespace URLs resolve from the staged directory and fall through to\n * the original fetch on a miss (a partially-staged namespace boots over the\n * wire instead of failing); a miss never touches the write-through cache —\n * for the own namespace the staged disk IS the resilience artifact. Foreign\n * `/bundles/…` URLs get the network with the stale-if-error cache; every\n * other URL falls through to the original fetch untouched. Inert when\n * neither a staged namespace nor a cache is configured.\n */\nexport function installGlobalBundleFetch(options: BundleFetchOptions): BundleFetchHandle {\n const namespace = options.namespace;\n const cacheDir = options.cacheDir;\n const runtime = namespace ? ManagedRuntime.make(BundleResolver.layer(namespace)) : null;\n const original: FetchImpl = globalThis.fetch;\n const patched = (async (input: Parameters<FetchImpl>[0], init?: Parameters<FetchImpl>[1]) => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n if (namespace && bundleUrlToLocalPath(url, namespace)) {\n if (!runtime) throw new Error(\"unreachable: resolver runtime missing\");\n const response = await runtime.runPromise(\n Effect.gen(function* () {\n const resolver = yield* BundleResolver;\n return yield* resolver.lookup(input);\n }),\n );\n if (response !== null) return response;\n return fetchWithTransientRetry(original, input, init);\n }\n\n if (cacheDir !== undefined && bundleCachePath(url, { cacheDir })) {\n return (async () => {\n try {\n const response = await fetchWithTransientRetry(original, input, init);\n if (response.ok) {\n try {\n const bytes = new Uint8Array(await response.clone().arrayBuffer());\n await writeBundleCache(url, bytes, { cacheDir });\n } catch {\n // write-through is best-effort\n }\n } else {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n }\n return response;\n } catch (error) {\n const cached = await readBundleCache(url, { cacheDir });\n if (cached) return staleResponse(cached, url);\n throw error;\n }\n })();\n }\n\n return fetchWithTransientRetry(original, input, init);\n }) as FetchImpl;\n const originalPreconnect = (original as Partial<FetchImpl>).preconnect;\n if (originalPreconnect) {\n patched.preconnect = originalPreconnect.bind(original);\n }\n globalThis.fetch = patched;\n return {\n uninstall: async () => {\n if (globalThis.fetch === patched) {\n globalThis.fetch = original;\n }\n if (runtime) await runtime.dispose();\n },\n };\n}\n\n/**\n * Boot-time installer for the CLI: derives the own namespace from\n * `BOS_BUNDLE_DIR` plus the runtime identity (`BOS_ACCOUNT`/`BOS_GATEWAY`\n * registry env taking precedence over the bos.config.json fields), and the\n * foreign-namespace cache from `BOS_BUNDLE_CACHE_DIR`. Tier auto-detection\n * (ADR 0021): an identity whose namespace is not actually staged under the\n * bundle dir drops to the registry tier — plain network fetch — instead of\n * deterministically 404ing own-namespace URLs against a foreign image's\n * staged bytes. Never throws — a missing or malformed identity leaves the\n * default network fetch in place.\n */\nexport function installBundleFetchFromEnv(input: {\n configPath?: string | null;\n}): BundleFetchHandle | null {\n const bundleDir = process.env.BOS_BUNDLE_DIR;\n const cacheDir = process.env.BOS_BUNDLE_CACHE_DIR;\n if (!bundleDir && cacheDir === undefined) return null;\n\n let configAccount: string | undefined;\n let configGateway: string | undefined;\n const configPath = input.configPath;\n if (configPath && !isAppDescriptorPath(configPath)) {\n try {\n const parsed = JSON.parse(readFileSync(configPath, \"utf8\")) as {\n account?: string;\n domain?: string;\n };\n configAccount = parsed.account;\n configGateway = parsed.domain;\n } catch {\n // unreadable config — identity may still come from the registry env\n }\n }\n\n const account = process.env.BOS_ACCOUNT ?? configAccount;\n const gateway = process.env.BOS_GATEWAY ?? configGateway;\n if (bundleDir && (!account || !gateway)) return null;\n\n let namespace: BundleNamespace | undefined;\n if (bundleDir && account && gateway) {\n if (existsSync(path.join(bundleDir, account, gateway))) {\n namespace = { bundleDir, account, gateway };\n } else {\n console.warn(\n `[bundles] staged namespace ${account}/${gateway} not found under ${bundleDir} — registry tier (network fetch)`,\n );\n }\n }\n return installGlobalBundleFetch({\n namespace,\n cacheDir: cacheDir ?? (namespace ? bundleCacheRoot() : undefined),\n });\n}\n"],"mappings":"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"}
@@ -20,6 +20,8 @@ const DEV_API_DB_PORT = 5432;
20
20
  const DEV_AUTH_DB_PORT = 5433;
21
21
  const TEST_API_DB_PORT = 5434;
22
22
  const TEST_AUTH_DB_PORT = 5435;
23
+ const CI_POSTGRES_IMAGE = "ghcr.io/nearbuilders/mirror-postgres:17-alpine";
24
+ const CI_REDIS_IMAGE = "ghcr.io/nearbuilders/mirror-redis:7-alpine";
23
25
  function uniqueSecrets(values) {
24
26
  const secrets = [];
25
27
  const seen = /* @__PURE__ */ new Set();
@@ -189,7 +191,7 @@ function buildCiInfraPlan(runtimeConfig, options = {}) {
189
191
  services.push({
190
192
  key: db.slug,
191
193
  slug: db.slug,
192
- image: "public.ecr.aws/docker/library/postgres:17-alpine",
194
+ image: CI_POSTGRES_IMAGE,
193
195
  env: {
194
196
  POSTGRES_USER,
195
197
  POSTGRES_PASSWORD,
@@ -213,7 +215,7 @@ function buildCiInfraPlan(runtimeConfig, options = {}) {
213
215
  for (const r of redisConfigs) services.push({
214
216
  key: r.slug,
215
217
  slug: r.slug,
216
- image: "public.ecr.aws/docker/library/redis:7-alpine",
218
+ image: CI_REDIS_IMAGE,
217
219
  env: {},
218
220
  ports: [`${r.port}:6379`],
219
221
  healthcheck: {
@@ -1 +1 @@
1
- {"version":3,"file":"infra.cjs","names":["join","existsSync","readFileSync","dirname","randomBytes"],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst REDIS_PORT = 6379;\nconst TEST_AUTH_SECRET = \"regression-test-secret-do-not-use-in-production\";\nconst VESTIGIAL_SECRETS = new Set([\n \"NEAR_RELAYER_PRIVATE_KEY_MAINNET\",\n \"NEAR_RELAYER_PRIVATE_KEY_TESTNET\",\n]);\n\n/**\n * Fixed local-development database conventions. The committed\n * docker-compose.yml provisions exactly these services/ports — nothing is\n * derived from runtime config. Every non-auth `*_DATABASE_URL` secret shares\n * the api database (schema-isolated per plugin); auth gets its own.\n */\nconst DEV_API_DB_PORT = 5432;\nconst DEV_AUTH_DB_PORT = 5433;\nconst TEST_API_DB_PORT = 5434;\nconst TEST_AUTH_DB_PORT = 5435;\n\nexport interface DatabaseConvention {\n secret: string;\n slug: string;\n port: number;\n databaseName: string;\n url: string;\n}\n\nexport interface RedisConvention {\n secret: string;\n slug: string;\n port: number;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport type { DevPortState };\n\nexport interface PortState {\n devPorts?: DevPortState;\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return {};\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return {};\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return { devPorts: raw.devPorts };\n } catch {\n return {};\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nfunction normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nfunction databaseUrl(port: number, databaseName: string): string {\n return `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${databaseName}`;\n}\n\nexport function buildConventionalDatabases(secrets: string[]): DatabaseConvention[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n return databaseSecrets\n .sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)))\n .map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? DEV_AUTH_DB_PORT : DEV_API_DB_PORT;\n const databaseName = isAuth ? \"auth_db\" : \"api_db\";\n return { secret, slug, port, databaseName, url: databaseUrl(port, databaseName) };\n });\n}\n\nexport function buildConventionalRedis(secrets: string[]): RedisConvention[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n\n return redisSecrets\n .sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)))\n .map((secret) => ({\n secret,\n slug: normalizeRedisSlug(secret),\n port: REDIS_PORT,\n url: `redis://localhost:${REDIS_PORT}`,\n }));\n}\n\nfunction testDatabaseUrl(secret: string): string {\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? TEST_AUTH_DB_PORT : TEST_API_DB_PORT;\n const databaseName = isAuth ? \"auth_test_db\" : \"api_test_db\";\n return databaseUrl(port, databaseName);\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (VESTIGIAL_SECRETS.has(s)) return false;\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nexport { resolveDevHostPort };\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseConvention>,\n redisConfigs: Map<string, RedisConvention>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (options.forExample) {\n return \"http://localhost:3000\";\n }\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\n/**\n * Plugin `*_DATABASE_URL` secrets share the API database (the platform's\n * two-database contract), so they are not rendered as template entries —\n * the host falls back to `API_DATABASE_URL` when they are unset.\n */\nfunction isFallbackDatabaseSecret(secret: string): boolean {\n return (\n secret.endsWith(\"_DATABASE_URL\") &&\n secret !== \"API_DATABASE_URL\" &&\n secret !== \"AUTH_DATABASE_URL\"\n );\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const rendered = groups\n .map((group) => ({\n section: group.section,\n secrets: group.secrets.filter((secret) => !isFallbackDatabaseSecret(secret)),\n }))\n .filter((group) => group.secrets.length > 0);\n const allSecrets = rendered.flatMap((group) => group.secrets);\n const databaseMap = new Map(\n buildConventionalDatabases(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const redisMap = new Map(\n buildConventionalRedis(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of rendered) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvFile };\n\nfunction renderEnvTestFile(groups: SecretGroup[]): string {\n const lines: string[] = [\n \"# Generated test environment — loaded by test suites instead of .env\",\n \"# Test databases run on dedicated *-test docker-compose services\",\n \"\",\n ];\n\n for (const group of groups) {\n const entries: string[] = [];\n for (const secret of group.secrets) {\n if (secret === \"BETTER_AUTH_SECRET\") {\n entries.push(`${secret}=${TEST_AUTH_SECRET}`);\n } else if (secret.endsWith(\"_DATABASE_URL\")) {\n entries.push(`${secret}=${testDatabaseUrl(secret)}`);\n }\n }\n if (entries.length > 0) {\n lines.push(`# ${group.section}`);\n lines.push(...entries);\n lines.push(\"\");\n }\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvTestFile };\n\nexport interface CiServiceSpec {\n key: string;\n slug: string;\n image: string;\n env: Record<string, string>;\n ports: string[];\n healthcheck?: { test: string[]; interval: string; timeout: string; retries: number };\n volumes: string[];\n database?: { user: string; password: string; name: string };\n}\n\nexport interface CiInfraPlan {\n account: string;\n gateway: string;\n project: string;\n env: Record<string, string>;\n services: CiServiceSpec[];\n generatedAt: string;\n}\n\nexport function buildCiInfraPlan(\n runtimeConfig: RuntimeConfig,\n options: { hostPortOverride?: number } = {},\n): CiInfraPlan {\n const allSecrets = collectAllSecrets(runtimeConfig);\n const databases = buildConventionalDatabases(allSecrets);\n const redisConfigs = buildConventionalRedis(allSecrets);\n\n const hostPort =\n options.hostPortOverride ??\n (Number.isFinite(Number(process.env.BOS_CI_HOST_PORT))\n ? Number(process.env.BOS_CI_HOST_PORT)\n : undefined) ??\n resolveDevHostPort(runtimeConfig);\n const env: Record<string, string> = {};\n for (const db of databases) env[db.secret] = db.url;\n for (const r of redisConfigs) env[r.secret] = r.url;\n\n env.CORS_ORIGIN = `http://127.0.0.1:${hostPort}`;\n if (!env.BETTER_AUTH_SECRET) env.BETTER_AUTH_SECRET = \"\";\n\n const services: CiServiceSpec[] = [];\n const seenPorts = new Set<number>();\n\n for (const db of databases) {\n if (seenPorts.has(db.port)) continue;\n seenPorts.add(db.port);\n services.push({\n key: db.slug,\n slug: db.slug,\n image: \"public.ecr.aws/docker/library/postgres:17-alpine\",\n env: {\n POSTGRES_USER: POSTGRES_USER,\n POSTGRES_PASSWORD: POSTGRES_PASSWORD,\n POSTGRES_DB: db.databaseName,\n },\n ports: [`${db.port}:5432`],\n healthcheck: {\n test: [\"CMD-SHELL\", `pg_isready -U ${POSTGRES_USER} -d ${db.databaseName}`],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${db.slug}_data:/var/lib/postgresql/data`],\n database: { user: POSTGRES_USER, password: POSTGRES_PASSWORD, name: db.databaseName },\n });\n }\n\n for (const r of redisConfigs) {\n services.push({\n key: r.slug,\n slug: r.slug,\n image: \"public.ecr.aws/docker/library/redis:7-alpine\",\n env: {},\n ports: [`${r.port}:6379`],\n healthcheck: {\n test: [\"CMD\", \"redis-cli\", \"ping\"],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${r.slug}_data:/data`],\n });\n }\n\n return {\n account: runtimeConfig.account,\n gateway: runtimeConfig.domain ?? runtimeConfig.account,\n project: runtimeConfig.account,\n env,\n services,\n generatedAt: new Date().toISOString(),\n };\n}\n\nfunction collectAllSecrets(runtimeConfig: RuntimeConfig): string[] {\n const all: string[] = [];\n all.push(...(runtimeConfig.host.secrets ?? []));\n all.push(...(runtimeConfig.api.secrets ?? []));\n if (runtimeConfig.auth) all.push(...(runtimeConfig.auth.secrets ?? []));\n if (runtimeConfig.plugins) {\n for (const plugin of Object.values(runtimeConfig.plugins)) {\n if (plugin.secrets) all.push(...plugin.secrets);\n }\n }\n return all.filter((s) => !VESTIGIAL_SECRETS.has(s));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"infra.cjs","names":["join","existsSync","readFileSync","dirname","randomBytes"],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst REDIS_PORT = 6379;\nconst TEST_AUTH_SECRET = \"regression-test-secret-do-not-use-in-production\";\nconst VESTIGIAL_SECRETS = new Set([\n \"NEAR_RELAYER_PRIVATE_KEY_MAINNET\",\n \"NEAR_RELAYER_PRIVATE_KEY_TESTNET\",\n]);\n\n/**\n * Fixed local-development database conventions. The committed\n * docker-compose.yml provisions exactly these services/ports — nothing is\n * derived from runtime config. Every non-auth `*_DATABASE_URL` secret shares\n * the api database (schema-isolated per plugin); auth gets its own.\n */\nconst DEV_API_DB_PORT = 5432;\nconst DEV_AUTH_DB_PORT = 5433;\nconst TEST_API_DB_PORT = 5434;\nconst TEST_AUTH_DB_PORT = 5435;\n\n// The org's GHCR mirror of the upstream library images (refreshed by\n// .github/workflows/mirror-images.yml) — Docker Hub and ECR Public both\n// rate-limit shared CI egress IPs.\nconst CI_POSTGRES_IMAGE = \"ghcr.io/nearbuilders/mirror-postgres:17-alpine\";\nconst CI_REDIS_IMAGE = \"ghcr.io/nearbuilders/mirror-redis:7-alpine\";\n\nexport interface DatabaseConvention {\n secret: string;\n slug: string;\n port: number;\n databaseName: string;\n url: string;\n}\n\nexport interface RedisConvention {\n secret: string;\n slug: string;\n port: number;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport type { DevPortState };\n\nexport interface PortState {\n devPorts?: DevPortState;\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return {};\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return {};\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return { devPorts: raw.devPorts };\n } catch {\n return {};\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nfunction normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nfunction databaseUrl(port: number, databaseName: string): string {\n return `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${databaseName}`;\n}\n\nexport function buildConventionalDatabases(secrets: string[]): DatabaseConvention[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n return databaseSecrets\n .sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)))\n .map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? DEV_AUTH_DB_PORT : DEV_API_DB_PORT;\n const databaseName = isAuth ? \"auth_db\" : \"api_db\";\n return { secret, slug, port, databaseName, url: databaseUrl(port, databaseName) };\n });\n}\n\nexport function buildConventionalRedis(secrets: string[]): RedisConvention[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n\n return redisSecrets\n .sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)))\n .map((secret) => ({\n secret,\n slug: normalizeRedisSlug(secret),\n port: REDIS_PORT,\n url: `redis://localhost:${REDIS_PORT}`,\n }));\n}\n\nfunction testDatabaseUrl(secret: string): string {\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? TEST_AUTH_DB_PORT : TEST_API_DB_PORT;\n const databaseName = isAuth ? \"auth_test_db\" : \"api_test_db\";\n return databaseUrl(port, databaseName);\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (VESTIGIAL_SECRETS.has(s)) return false;\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nexport { resolveDevHostPort };\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseConvention>,\n redisConfigs: Map<string, RedisConvention>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (options.forExample) {\n return \"http://localhost:3000\";\n }\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\n/**\n * Plugin `*_DATABASE_URL` secrets share the API database (the platform's\n * two-database contract), so they are not rendered as template entries —\n * the host falls back to `API_DATABASE_URL` when they are unset.\n */\nfunction isFallbackDatabaseSecret(secret: string): boolean {\n return (\n secret.endsWith(\"_DATABASE_URL\") &&\n secret !== \"API_DATABASE_URL\" &&\n secret !== \"AUTH_DATABASE_URL\"\n );\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const rendered = groups\n .map((group) => ({\n section: group.section,\n secrets: group.secrets.filter((secret) => !isFallbackDatabaseSecret(secret)),\n }))\n .filter((group) => group.secrets.length > 0);\n const allSecrets = rendered.flatMap((group) => group.secrets);\n const databaseMap = new Map(\n buildConventionalDatabases(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const redisMap = new Map(\n buildConventionalRedis(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of rendered) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvFile };\n\nfunction renderEnvTestFile(groups: SecretGroup[]): string {\n const lines: string[] = [\n \"# Generated test environment — loaded by test suites instead of .env\",\n \"# Test databases run on dedicated *-test docker-compose services\",\n \"\",\n ];\n\n for (const group of groups) {\n const entries: string[] = [];\n for (const secret of group.secrets) {\n if (secret === \"BETTER_AUTH_SECRET\") {\n entries.push(`${secret}=${TEST_AUTH_SECRET}`);\n } else if (secret.endsWith(\"_DATABASE_URL\")) {\n entries.push(`${secret}=${testDatabaseUrl(secret)}`);\n }\n }\n if (entries.length > 0) {\n lines.push(`# ${group.section}`);\n lines.push(...entries);\n lines.push(\"\");\n }\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvTestFile };\n\nexport interface CiServiceSpec {\n key: string;\n slug: string;\n image: string;\n env: Record<string, string>;\n ports: string[];\n healthcheck?: { test: string[]; interval: string; timeout: string; retries: number };\n volumes: string[];\n database?: { user: string; password: string; name: string };\n}\n\nexport interface CiInfraPlan {\n account: string;\n gateway: string;\n project: string;\n env: Record<string, string>;\n services: CiServiceSpec[];\n generatedAt: string;\n}\n\nexport function buildCiInfraPlan(\n runtimeConfig: RuntimeConfig,\n options: { hostPortOverride?: number } = {},\n): CiInfraPlan {\n const allSecrets = collectAllSecrets(runtimeConfig);\n const databases = buildConventionalDatabases(allSecrets);\n const redisConfigs = buildConventionalRedis(allSecrets);\n\n const hostPort =\n options.hostPortOverride ??\n (Number.isFinite(Number(process.env.BOS_CI_HOST_PORT))\n ? Number(process.env.BOS_CI_HOST_PORT)\n : undefined) ??\n resolveDevHostPort(runtimeConfig);\n const env: Record<string, string> = {};\n for (const db of databases) env[db.secret] = db.url;\n for (const r of redisConfigs) env[r.secret] = r.url;\n\n env.CORS_ORIGIN = `http://127.0.0.1:${hostPort}`;\n if (!env.BETTER_AUTH_SECRET) env.BETTER_AUTH_SECRET = \"\";\n\n const services: CiServiceSpec[] = [];\n const seenPorts = new Set<number>();\n\n for (const db of databases) {\n if (seenPorts.has(db.port)) continue;\n seenPorts.add(db.port);\n services.push({\n key: db.slug,\n slug: db.slug,\n image: CI_POSTGRES_IMAGE,\n env: {\n POSTGRES_USER: POSTGRES_USER,\n POSTGRES_PASSWORD: POSTGRES_PASSWORD,\n POSTGRES_DB: db.databaseName,\n },\n ports: [`${db.port}:5432`],\n healthcheck: {\n test: [\"CMD-SHELL\", `pg_isready -U ${POSTGRES_USER} -d ${db.databaseName}`],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${db.slug}_data:/var/lib/postgresql/data`],\n database: { user: POSTGRES_USER, password: POSTGRES_PASSWORD, name: db.databaseName },\n });\n }\n\n for (const r of redisConfigs) {\n services.push({\n key: r.slug,\n slug: r.slug,\n image: CI_REDIS_IMAGE,\n env: {},\n ports: [`${r.port}:6379`],\n healthcheck: {\n test: [\"CMD\", \"redis-cli\", \"ping\"],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${r.slug}_data:/data`],\n });\n }\n\n return {\n account: runtimeConfig.account,\n gateway: runtimeConfig.domain ?? runtimeConfig.account,\n project: runtimeConfig.account,\n env,\n services,\n generatedAt: new Date().toISOString(),\n };\n}\n\nfunction collectAllSecrets(runtimeConfig: RuntimeConfig): string[] {\n const all: string[] = [];\n all.push(...(runtimeConfig.host.secrets ?? []));\n all.push(...(runtimeConfig.api.secrets ?? []));\n if (runtimeConfig.auth) all.push(...(runtimeConfig.auth.secrets ?? []));\n if (runtimeConfig.plugins) {\n for (const plugin of Object.values(runtimeConfig.plugins)) {\n if (plugin.secrets) all.push(...plugin.secrets);\n }\n }\n return all.filter((s) => !VESTIGIAL_SECRETS.has(s));\n}\n"],"mappings":"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"}
@@ -20,6 +20,8 @@ const DEV_API_DB_PORT = 5432;
20
20
  const DEV_AUTH_DB_PORT = 5433;
21
21
  const TEST_API_DB_PORT = 5434;
22
22
  const TEST_AUTH_DB_PORT = 5435;
23
+ const CI_POSTGRES_IMAGE = "ghcr.io/nearbuilders/mirror-postgres:17-alpine";
24
+ const CI_REDIS_IMAGE = "ghcr.io/nearbuilders/mirror-redis:7-alpine";
23
25
  function uniqueSecrets(values) {
24
26
  const secrets = [];
25
27
  const seen = /* @__PURE__ */ new Set();
@@ -189,7 +191,7 @@ function buildCiInfraPlan(runtimeConfig, options = {}) {
189
191
  services.push({
190
192
  key: db.slug,
191
193
  slug: db.slug,
192
- image: "public.ecr.aws/docker/library/postgres:17-alpine",
194
+ image: CI_POSTGRES_IMAGE,
193
195
  env: {
194
196
  POSTGRES_USER,
195
197
  POSTGRES_PASSWORD,
@@ -213,7 +215,7 @@ function buildCiInfraPlan(runtimeConfig, options = {}) {
213
215
  for (const r of redisConfigs) services.push({
214
216
  key: r.slug,
215
217
  slug: r.slug,
216
- image: "public.ecr.aws/docker/library/redis:7-alpine",
218
+ image: CI_REDIS_IMAGE,
217
219
  env: {},
218
220
  ports: [`${r.port}:6379`],
219
221
  healthcheck: {
@@ -1 +1 @@
1
- {"version":3,"file":"infra.mjs","names":[],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst REDIS_PORT = 6379;\nconst TEST_AUTH_SECRET = \"regression-test-secret-do-not-use-in-production\";\nconst VESTIGIAL_SECRETS = new Set([\n \"NEAR_RELAYER_PRIVATE_KEY_MAINNET\",\n \"NEAR_RELAYER_PRIVATE_KEY_TESTNET\",\n]);\n\n/**\n * Fixed local-development database conventions. The committed\n * docker-compose.yml provisions exactly these services/ports — nothing is\n * derived from runtime config. Every non-auth `*_DATABASE_URL` secret shares\n * the api database (schema-isolated per plugin); auth gets its own.\n */\nconst DEV_API_DB_PORT = 5432;\nconst DEV_AUTH_DB_PORT = 5433;\nconst TEST_API_DB_PORT = 5434;\nconst TEST_AUTH_DB_PORT = 5435;\n\nexport interface DatabaseConvention {\n secret: string;\n slug: string;\n port: number;\n databaseName: string;\n url: string;\n}\n\nexport interface RedisConvention {\n secret: string;\n slug: string;\n port: number;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport type { DevPortState };\n\nexport interface PortState {\n devPorts?: DevPortState;\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return {};\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return {};\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return { devPorts: raw.devPorts };\n } catch {\n return {};\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nfunction normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nfunction databaseUrl(port: number, databaseName: string): string {\n return `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${databaseName}`;\n}\n\nexport function buildConventionalDatabases(secrets: string[]): DatabaseConvention[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n return databaseSecrets\n .sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)))\n .map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? DEV_AUTH_DB_PORT : DEV_API_DB_PORT;\n const databaseName = isAuth ? \"auth_db\" : \"api_db\";\n return { secret, slug, port, databaseName, url: databaseUrl(port, databaseName) };\n });\n}\n\nexport function buildConventionalRedis(secrets: string[]): RedisConvention[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n\n return redisSecrets\n .sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)))\n .map((secret) => ({\n secret,\n slug: normalizeRedisSlug(secret),\n port: REDIS_PORT,\n url: `redis://localhost:${REDIS_PORT}`,\n }));\n}\n\nfunction testDatabaseUrl(secret: string): string {\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? TEST_AUTH_DB_PORT : TEST_API_DB_PORT;\n const databaseName = isAuth ? \"auth_test_db\" : \"api_test_db\";\n return databaseUrl(port, databaseName);\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (VESTIGIAL_SECRETS.has(s)) return false;\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nexport { resolveDevHostPort };\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseConvention>,\n redisConfigs: Map<string, RedisConvention>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (options.forExample) {\n return \"http://localhost:3000\";\n }\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\n/**\n * Plugin `*_DATABASE_URL` secrets share the API database (the platform's\n * two-database contract), so they are not rendered as template entries —\n * the host falls back to `API_DATABASE_URL` when they are unset.\n */\nfunction isFallbackDatabaseSecret(secret: string): boolean {\n return (\n secret.endsWith(\"_DATABASE_URL\") &&\n secret !== \"API_DATABASE_URL\" &&\n secret !== \"AUTH_DATABASE_URL\"\n );\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const rendered = groups\n .map((group) => ({\n section: group.section,\n secrets: group.secrets.filter((secret) => !isFallbackDatabaseSecret(secret)),\n }))\n .filter((group) => group.secrets.length > 0);\n const allSecrets = rendered.flatMap((group) => group.secrets);\n const databaseMap = new Map(\n buildConventionalDatabases(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const redisMap = new Map(\n buildConventionalRedis(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of rendered) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvFile };\n\nfunction renderEnvTestFile(groups: SecretGroup[]): string {\n const lines: string[] = [\n \"# Generated test environment — loaded by test suites instead of .env\",\n \"# Test databases run on dedicated *-test docker-compose services\",\n \"\",\n ];\n\n for (const group of groups) {\n const entries: string[] = [];\n for (const secret of group.secrets) {\n if (secret === \"BETTER_AUTH_SECRET\") {\n entries.push(`${secret}=${TEST_AUTH_SECRET}`);\n } else if (secret.endsWith(\"_DATABASE_URL\")) {\n entries.push(`${secret}=${testDatabaseUrl(secret)}`);\n }\n }\n if (entries.length > 0) {\n lines.push(`# ${group.section}`);\n lines.push(...entries);\n lines.push(\"\");\n }\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvTestFile };\n\nexport interface CiServiceSpec {\n key: string;\n slug: string;\n image: string;\n env: Record<string, string>;\n ports: string[];\n healthcheck?: { test: string[]; interval: string; timeout: string; retries: number };\n volumes: string[];\n database?: { user: string; password: string; name: string };\n}\n\nexport interface CiInfraPlan {\n account: string;\n gateway: string;\n project: string;\n env: Record<string, string>;\n services: CiServiceSpec[];\n generatedAt: string;\n}\n\nexport function buildCiInfraPlan(\n runtimeConfig: RuntimeConfig,\n options: { hostPortOverride?: number } = {},\n): CiInfraPlan {\n const allSecrets = collectAllSecrets(runtimeConfig);\n const databases = buildConventionalDatabases(allSecrets);\n const redisConfigs = buildConventionalRedis(allSecrets);\n\n const hostPort =\n options.hostPortOverride ??\n (Number.isFinite(Number(process.env.BOS_CI_HOST_PORT))\n ? Number(process.env.BOS_CI_HOST_PORT)\n : undefined) ??\n resolveDevHostPort(runtimeConfig);\n const env: Record<string, string> = {};\n for (const db of databases) env[db.secret] = db.url;\n for (const r of redisConfigs) env[r.secret] = r.url;\n\n env.CORS_ORIGIN = `http://127.0.0.1:${hostPort}`;\n if (!env.BETTER_AUTH_SECRET) env.BETTER_AUTH_SECRET = \"\";\n\n const services: CiServiceSpec[] = [];\n const seenPorts = new Set<number>();\n\n for (const db of databases) {\n if (seenPorts.has(db.port)) continue;\n seenPorts.add(db.port);\n services.push({\n key: db.slug,\n slug: db.slug,\n image: \"public.ecr.aws/docker/library/postgres:17-alpine\",\n env: {\n POSTGRES_USER: POSTGRES_USER,\n POSTGRES_PASSWORD: POSTGRES_PASSWORD,\n POSTGRES_DB: db.databaseName,\n },\n ports: [`${db.port}:5432`],\n healthcheck: {\n test: [\"CMD-SHELL\", `pg_isready -U ${POSTGRES_USER} -d ${db.databaseName}`],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${db.slug}_data:/var/lib/postgresql/data`],\n database: { user: POSTGRES_USER, password: POSTGRES_PASSWORD, name: db.databaseName },\n });\n }\n\n for (const r of redisConfigs) {\n services.push({\n key: r.slug,\n slug: r.slug,\n image: \"public.ecr.aws/docker/library/redis:7-alpine\",\n env: {},\n ports: [`${r.port}:6379`],\n healthcheck: {\n test: [\"CMD\", \"redis-cli\", \"ping\"],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${r.slug}_data:/data`],\n });\n }\n\n return {\n account: runtimeConfig.account,\n gateway: runtimeConfig.domain ?? runtimeConfig.account,\n project: runtimeConfig.account,\n env,\n services,\n generatedAt: new Date().toISOString(),\n };\n}\n\nfunction collectAllSecrets(runtimeConfig: RuntimeConfig): string[] {\n const all: string[] = [];\n all.push(...(runtimeConfig.host.secrets ?? []));\n all.push(...(runtimeConfig.api.secrets ?? []));\n if (runtimeConfig.auth) all.push(...(runtimeConfig.auth.secrets ?? []));\n if (runtimeConfig.plugins) {\n for (const plugin of Object.values(runtimeConfig.plugins)) {\n if (plugin.secrets) all.push(...plugin.secrets);\n }\n }\n return all.filter((s) => !VESTIGIAL_SECRETS.has(s));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"infra.mjs","names":[],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst REDIS_PORT = 6379;\nconst TEST_AUTH_SECRET = \"regression-test-secret-do-not-use-in-production\";\nconst VESTIGIAL_SECRETS = new Set([\n \"NEAR_RELAYER_PRIVATE_KEY_MAINNET\",\n \"NEAR_RELAYER_PRIVATE_KEY_TESTNET\",\n]);\n\n/**\n * Fixed local-development database conventions. The committed\n * docker-compose.yml provisions exactly these services/ports — nothing is\n * derived from runtime config. Every non-auth `*_DATABASE_URL` secret shares\n * the api database (schema-isolated per plugin); auth gets its own.\n */\nconst DEV_API_DB_PORT = 5432;\nconst DEV_AUTH_DB_PORT = 5433;\nconst TEST_API_DB_PORT = 5434;\nconst TEST_AUTH_DB_PORT = 5435;\n\n// The org's GHCR mirror of the upstream library images (refreshed by\n// .github/workflows/mirror-images.yml) — Docker Hub and ECR Public both\n// rate-limit shared CI egress IPs.\nconst CI_POSTGRES_IMAGE = \"ghcr.io/nearbuilders/mirror-postgres:17-alpine\";\nconst CI_REDIS_IMAGE = \"ghcr.io/nearbuilders/mirror-redis:7-alpine\";\n\nexport interface DatabaseConvention {\n secret: string;\n slug: string;\n port: number;\n databaseName: string;\n url: string;\n}\n\nexport interface RedisConvention {\n secret: string;\n slug: string;\n port: number;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport type { DevPortState };\n\nexport interface PortState {\n devPorts?: DevPortState;\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return {};\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return {};\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return { devPorts: raw.devPorts };\n } catch {\n return {};\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nfunction normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nfunction databaseUrl(port: number, databaseName: string): string {\n return `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${databaseName}`;\n}\n\nexport function buildConventionalDatabases(secrets: string[]): DatabaseConvention[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n return databaseSecrets\n .sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)))\n .map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? DEV_AUTH_DB_PORT : DEV_API_DB_PORT;\n const databaseName = isAuth ? \"auth_db\" : \"api_db\";\n return { secret, slug, port, databaseName, url: databaseUrl(port, databaseName) };\n });\n}\n\nexport function buildConventionalRedis(secrets: string[]): RedisConvention[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n\n return redisSecrets\n .sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)))\n .map((secret) => ({\n secret,\n slug: normalizeRedisSlug(secret),\n port: REDIS_PORT,\n url: `redis://localhost:${REDIS_PORT}`,\n }));\n}\n\nfunction testDatabaseUrl(secret: string): string {\n const isAuth = secret === AUTH_DATABASE_SECRET;\n const port = isAuth ? TEST_AUTH_DB_PORT : TEST_API_DB_PORT;\n const databaseName = isAuth ? \"auth_test_db\" : \"api_test_db\";\n return databaseUrl(port, databaseName);\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (VESTIGIAL_SECRETS.has(s)) return false;\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nexport { resolveDevHostPort };\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseConvention>,\n redisConfigs: Map<string, RedisConvention>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (options.forExample) {\n return \"http://localhost:3000\";\n }\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\n/**\n * Plugin `*_DATABASE_URL` secrets share the API database (the platform's\n * two-database contract), so they are not rendered as template entries —\n * the host falls back to `API_DATABASE_URL` when they are unset.\n */\nfunction isFallbackDatabaseSecret(secret: string): boolean {\n return (\n secret.endsWith(\"_DATABASE_URL\") &&\n secret !== \"API_DATABASE_URL\" &&\n secret !== \"AUTH_DATABASE_URL\"\n );\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const rendered = groups\n .map((group) => ({\n section: group.section,\n secrets: group.secrets.filter((secret) => !isFallbackDatabaseSecret(secret)),\n }))\n .filter((group) => group.secrets.length > 0);\n const allSecrets = rendered.flatMap((group) => group.secrets);\n const databaseMap = new Map(\n buildConventionalDatabases(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const redisMap = new Map(\n buildConventionalRedis(allSecrets).map((entry) => [entry.secret, entry]),\n );\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of rendered) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvFile };\n\nfunction renderEnvTestFile(groups: SecretGroup[]): string {\n const lines: string[] = [\n \"# Generated test environment — loaded by test suites instead of .env\",\n \"# Test databases run on dedicated *-test docker-compose services\",\n \"\",\n ];\n\n for (const group of groups) {\n const entries: string[] = [];\n for (const secret of group.secrets) {\n if (secret === \"BETTER_AUTH_SECRET\") {\n entries.push(`${secret}=${TEST_AUTH_SECRET}`);\n } else if (secret.endsWith(\"_DATABASE_URL\")) {\n entries.push(`${secret}=${testDatabaseUrl(secret)}`);\n }\n }\n if (entries.length > 0) {\n lines.push(`# ${group.section}`);\n lines.push(...entries);\n lines.push(\"\");\n }\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvTestFile };\n\nexport interface CiServiceSpec {\n key: string;\n slug: string;\n image: string;\n env: Record<string, string>;\n ports: string[];\n healthcheck?: { test: string[]; interval: string; timeout: string; retries: number };\n volumes: string[];\n database?: { user: string; password: string; name: string };\n}\n\nexport interface CiInfraPlan {\n account: string;\n gateway: string;\n project: string;\n env: Record<string, string>;\n services: CiServiceSpec[];\n generatedAt: string;\n}\n\nexport function buildCiInfraPlan(\n runtimeConfig: RuntimeConfig,\n options: { hostPortOverride?: number } = {},\n): CiInfraPlan {\n const allSecrets = collectAllSecrets(runtimeConfig);\n const databases = buildConventionalDatabases(allSecrets);\n const redisConfigs = buildConventionalRedis(allSecrets);\n\n const hostPort =\n options.hostPortOverride ??\n (Number.isFinite(Number(process.env.BOS_CI_HOST_PORT))\n ? Number(process.env.BOS_CI_HOST_PORT)\n : undefined) ??\n resolveDevHostPort(runtimeConfig);\n const env: Record<string, string> = {};\n for (const db of databases) env[db.secret] = db.url;\n for (const r of redisConfigs) env[r.secret] = r.url;\n\n env.CORS_ORIGIN = `http://127.0.0.1:${hostPort}`;\n if (!env.BETTER_AUTH_SECRET) env.BETTER_AUTH_SECRET = \"\";\n\n const services: CiServiceSpec[] = [];\n const seenPorts = new Set<number>();\n\n for (const db of databases) {\n if (seenPorts.has(db.port)) continue;\n seenPorts.add(db.port);\n services.push({\n key: db.slug,\n slug: db.slug,\n image: CI_POSTGRES_IMAGE,\n env: {\n POSTGRES_USER: POSTGRES_USER,\n POSTGRES_PASSWORD: POSTGRES_PASSWORD,\n POSTGRES_DB: db.databaseName,\n },\n ports: [`${db.port}:5432`],\n healthcheck: {\n test: [\"CMD-SHELL\", `pg_isready -U ${POSTGRES_USER} -d ${db.databaseName}`],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${db.slug}_data:/var/lib/postgresql/data`],\n database: { user: POSTGRES_USER, password: POSTGRES_PASSWORD, name: db.databaseName },\n });\n }\n\n for (const r of redisConfigs) {\n services.push({\n key: r.slug,\n slug: r.slug,\n image: CI_REDIS_IMAGE,\n env: {},\n ports: [`${r.port}:6379`],\n healthcheck: {\n test: [\"CMD\", \"redis-cli\", \"ping\"],\n interval: \"3s\",\n timeout: \"3s\",\n retries: 10,\n },\n volumes: [`${r.slug}_data:/data`],\n });\n }\n\n return {\n account: runtimeConfig.account,\n gateway: runtimeConfig.domain ?? runtimeConfig.account,\n project: runtimeConfig.account,\n env,\n services,\n generatedAt: new Date().toISOString(),\n };\n}\n\nfunction collectAllSecrets(runtimeConfig: RuntimeConfig): string[] {\n const all: string[] = [];\n all.push(...(runtimeConfig.host.secrets ?? []));\n all.push(...(runtimeConfig.api.secrets ?? []));\n if (runtimeConfig.auth) all.push(...(runtimeConfig.auth.secrets ?? []));\n if (runtimeConfig.plugins) {\n for (const plugin of Object.values(runtimeConfig.plugins)) {\n if (plugin.secrets) all.push(...plugin.secrets);\n }\n }\n return all.filter((s) => !VESTIGIAL_SECRETS.has(s));\n}\n"],"mappings":"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"}
@@ -377,8 +377,7 @@ const startBootstrap = (deps, input) => effect.Effect.gen(function* () {
377
377
  },
378
378
  description: `${isStaging ? "Staging" : "Production"} Mode (${baseConfig.account})`,
379
379
  port: plan.resolvedPorts.host ?? port,
380
- interactive: input.interactive,
381
- noLogs: true
380
+ interactive: input.interactive
382
381
  };
383
382
  yield* require_progress.emitProgress({
384
383
  phase: "config",