everything-dev 2.0.0-rc.4 → 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.
Files changed (71) hide show
  1. package/dist/api-contract.cjs +3 -6
  2. package/dist/api-contract.cjs.map +1 -1
  3. package/dist/api-contract.mjs +3 -6
  4. package/dist/api-contract.mjs.map +1 -1
  5. package/dist/apps/upload/src/client.cjs +188 -0
  6. package/dist/apps/upload/src/client.cjs.map +1 -0
  7. package/dist/apps/upload/src/client.mjs +180 -0
  8. package/dist/apps/upload/src/client.mjs.map +1 -0
  9. package/dist/bundle-fs-resolve.cjs +37 -3
  10. package/dist/bundle-fs-resolve.cjs.map +1 -1
  11. package/dist/bundle-fs-resolve.mjs +37 -3
  12. package/dist/bundle-fs-resolve.mjs.map +1 -1
  13. package/dist/cdn-deploy.cjs +29 -11
  14. package/dist/cdn-deploy.cjs.map +1 -1
  15. package/dist/cdn-deploy.mjs +29 -11
  16. package/dist/cdn-deploy.mjs.map +1 -1
  17. package/dist/cli/infra.cjs +4 -2
  18. package/dist/cli/infra.cjs.map +1 -1
  19. package/dist/cli/infra.mjs +4 -2
  20. package/dist/cli/infra.mjs.map +1 -1
  21. package/dist/cli/init.cjs +0 -2
  22. package/dist/cli/init.cjs.map +1 -1
  23. package/dist/cli/init.d.cts +1 -1
  24. package/dist/cli/init.d.mts +1 -1
  25. package/dist/cli/init.mjs +0 -2
  26. package/dist/cli/init.mjs.map +1 -1
  27. package/dist/cli/sync.cjs +0 -1
  28. package/dist/cli/sync.cjs.map +1 -1
  29. package/dist/cli/sync.mjs +0 -1
  30. package/dist/cli/sync.mjs.map +1 -1
  31. package/dist/commands/deploy.cjs +0 -2
  32. package/dist/commands/deploy.cjs.map +1 -1
  33. package/dist/commands/deploy.mjs +0 -2
  34. package/dist/commands/deploy.mjs.map +1 -1
  35. package/dist/commands/upgrade.cjs +30 -7
  36. package/dist/commands/upgrade.cjs.map +1 -1
  37. package/dist/commands/upgrade.mjs +30 -7
  38. package/dist/commands/upgrade.mjs.map +1 -1
  39. package/dist/config.cjs.map +1 -1
  40. package/dist/config.mjs.map +1 -1
  41. package/dist/contract.meta.cjs +1 -1
  42. package/dist/contract.meta.cjs.map +1 -1
  43. package/dist/contract.meta.d.cts +1 -1
  44. package/dist/contract.meta.d.mts +1 -1
  45. package/dist/contract.meta.mjs +1 -1
  46. package/dist/contract.meta.mjs.map +1 -1
  47. package/dist/dev-program.cjs +2 -2
  48. package/dist/dev-program.cjs.map +1 -1
  49. package/dist/dev-program.mjs +2 -3
  50. package/dist/dev-program.mjs.map +1 -1
  51. package/dist/image-deploy.cjs +38 -14
  52. package/dist/image-deploy.cjs.map +1 -1
  53. package/dist/image-deploy.mjs +39 -15
  54. package/dist/image-deploy.mjs.map +1 -1
  55. package/dist/orchestrator.cjs +22 -18
  56. package/dist/orchestrator.cjs.map +1 -1
  57. package/dist/orchestrator.mjs +22 -18
  58. package/dist/orchestrator.mjs.map +1 -1
  59. package/dist/storage-upload.cjs +7 -68
  60. package/dist/storage-upload.cjs.map +1 -1
  61. package/dist/storage-upload.mjs +7 -68
  62. package/dist/storage-upload.mjs.map +1 -1
  63. package/dist/ui/api.cjs +11 -69
  64. package/dist/ui/api.cjs.map +1 -1
  65. package/dist/ui/api.d.cts +2 -4
  66. package/dist/ui/api.d.mts +2 -4
  67. package/dist/ui/api.mjs +11 -69
  68. package/dist/ui/api.mjs.map +1 -1
  69. package/dist/ui/auth.d.cts +43 -43
  70. package/dist/ui/auth.d.mts +43 -43
  71. package/package.json +3 -2
@@ -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":"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"}
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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"}
@@ -73,7 +73,13 @@ function resolveCdnDeployInputs(input) {
73
73
  error: `The CLI session pins uploads to ${sessionSite} — a local site can never receive production bundles. Re-run \`bos login\` against the production site, or set BOS_STORAGE_ORIGIN explicitly.`
74
74
  };
75
75
  storageOrigin = sessionSite;
76
- } else storageOrigin = `https://${input.gateway}`;
76
+ } else return {
77
+ cdnOrigin,
78
+ storageOrigin: storageOriginDefault,
79
+ apiKey,
80
+ warning,
81
+ error: `Bundle uploads require an upload-service origin — the gateway (${input.gateway}) no longer serves uploads (ADR 0027: the upload service is standalone at upload.everything.dev). Set BOS_STORAGE_ORIGIN in the environment (CI) or re-run \`bos login\` against the production site so the session pins it.`
82
+ };
77
83
  if (!apiKey) return {
78
84
  cdnOrigin,
79
85
  storageOrigin,
@@ -96,27 +102,39 @@ function resolveCdnDeployInputs(input) {
96
102
  };
97
103
  }
98
104
  /**
99
- * Pre-upload probe: the storage origin must actually be the platform API.
100
- * `/.well-known/mcp.json` is the cheapest unauthenticated platform
101
- * fingerprint — a random service squatting the port (or a stale session
102
- * pointing at the wrong server) fails here before megabytes move.
105
+ * Pre-upload probe: the storage origin must actually be the bundle-upload
106
+ * service.
107
+ * `/health` is the primary readiness check, with MCP kept as a compatibility
108
+ * fallback for older fixtures and legacy hosts.
103
109
  */
104
110
  async function probeStorageOrigin(storageOrigin, fetchImpl = fetch) {
105
- const probeUrl = `${storageOrigin.replace(/\/$/, "")}/.well-known/mcp.json`;
111
+ const base = storageOrigin.replace(/\/$/, "");
112
+ const healthUrl = `${base}/health`;
113
+ const mcpUrl = `${base}/.well-known/mcp.json`;
106
114
  let response;
107
115
  try {
108
- response = await fetchImpl(probeUrl, { headers: { accept: "application/json" } });
116
+ response = await fetchImpl(healthUrl, { headers: { accept: "text/plain" } });
109
117
  } catch (error) {
110
118
  return `unreachable (${String(error instanceof Error ? error.message : error)})`;
111
119
  }
112
- if (!response.ok) return `GET /.well-known/mcp.json responded ${response.status}`;
113
- let body;
120
+ if (response.ok) {
121
+ const body = await response.text().catch(() => "");
122
+ if (body.trim() === "OK" || body.trim().length === 0) return void 0;
123
+ }
124
+ let mcpResponse;
125
+ try {
126
+ mcpResponse = await fetchImpl(mcpUrl, { headers: { accept: "application/json" } });
127
+ } catch {
128
+ return `GET /health responded ${response.status}; fallback /.well-known/mcp.json unreachable`;
129
+ }
130
+ if (!mcpResponse.ok) return `GET /health responded ${response.status}; GET /.well-known/mcp.json responded ${mcpResponse.status}`;
114
131
  try {
115
- body = await response.json();
132
+ const body = await mcpResponse.json();
133
+ if (!body || typeof body !== "object") return "did not return the platform MCP descriptor";
134
+ return;
116
135
  } catch {
117
136
  return "did not return the platform MCP descriptor (non-JSON response)";
118
137
  }
119
- if (!body || typeof body !== "object") return "did not return the platform MCP descriptor";
120
138
  }
121
139
 
122
140
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"cdn-deploy.cjs","names":[],"sources":["../src/cdn-deploy.ts"],"sourcesContent":["interface SessionCredentialLike {\n apiKey?: string;\n siteUrl?: string;\n accountId?: string | null;\n}\n\nexport interface CdnDeployInputs {\n cdnOrigin: string | undefined;\n storageOrigin: string;\n apiKey: string | undefined;\n warning?: string;\n error?: string;\n}\n\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\", \"0.0.0.0\", \"local\"]);\n\n/** Local/private destinations can never serve production bundles. */\nexport function isLocalOrigin(url: string): boolean {\n try {\n const { hostname, protocol } = new URL(url);\n if (protocol !== \"http:\" && protocol !== \"https:\") return true;\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n return (\n LOCAL_HOSTNAMES.has(host) ||\n host.endsWith(\".localhost\") ||\n host.endsWith(\".local\") ||\n /^127\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^10\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^192\\.168\\.\\d+\\.\\d+$/.test(host) ||\n /^172\\.(1[6-9]|2\\d|3[01])\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"169.254.\") ||\n (host.includes(\":\") &&\n (host.startsWith(\"fe80:\") || host.startsWith(\"fc\") || host.startsWith(\"fd\")))\n );\n } catch {\n return true;\n }\n}\n\n/**\n * Deploy origin resolution (ADR 0020, as amended): the CDN origin comes from\n * the env or the authored `cdn.origin` (inherited via extends) — a hard\n * requirement when uploads are planned, because the host never serves\n * `/bundles` itself. The upload origin is the env, the `bos login` session's\n * siteUrl, or the runtime's own gateway. A session pinned to a local site is\n * a hard error (the origin can only ever be a squatter's port); an env-pinned\n * local origin is deliberate local testing and only warns. Config-only\n * publishes (`uploadsPlanned: false`) ship the committed config verbatim —\n * no origins are resolved and nothing is required.\n */\nexport function resolveCdnDeployInputs(input: {\n env: Record<string, string | undefined>;\n bosConfig: { cdn?: { origin?: string } } | null;\n session: SessionCredentialLike | null;\n account: string;\n gateway: string;\n uploadsPlanned: boolean;\n}): CdnDeployInputs {\n const envCdnOrigin = input.env.BOS_BUNDLE_CDN_ORIGIN?.replace(/\\/$/, \"\");\n const configCdnOrigin = input.bosConfig?.cdn?.origin?.replace(/\\/$/, \"\");\n const storageEnvOrigin = input.env.BOS_STORAGE_ORIGIN?.replace(/\\/$/, \"\");\n const sessionSite = input.session?.siteUrl?.replace(/\\/$/, \"\");\n const apiKey = input.env.BOS_STORAGE_API_KEY ?? input.session?.apiKey;\n const uploadsPlanned = input.uploadsPlanned;\n\n const storageOriginDefault = storageEnvOrigin ?? sessionSite ?? `https://${input.gateway}`;\n\n if (!uploadsPlanned) {\n return { cdnOrigin: undefined, storageOrigin: storageOriginDefault, apiKey };\n }\n\n let cdnOrigin: string | undefined;\n let warning: string | undefined;\n if (envCdnOrigin) {\n cdnOrigin = envCdnOrigin;\n if (isLocalOrigin(envCdnOrigin)) {\n warning = `BOS_BUNDLE_CDN_ORIGIN is a local URL (${envCdnOrigin}) — deploying to a local stack, published bundle URLs will not resolve publicly.`;\n }\n } else if (configCdnOrigin) {\n if (isLocalOrigin(configCdnOrigin)) {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n `the authored config sets cdn.origin to the local URL ${configCdnOrigin} — deploy bundle ` +\n \"origins must be publicly reachable. Set BOS_BUNDLE_CDN_ORIGIN for a local deploy, \" +\n \"or fix cdn.origin (inherited via extends).\",\n };\n }\n cdnOrigin = configCdnOrigin;\n } else {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n \"CDN deploy requires a bundle origin — set `cdn.origin` in bos.config.json \" +\n \"(children inherit it from the base runtime via extends) or BOS_BUNDLE_CDN_ORIGIN \" +\n \"in the environment.\",\n };\n }\n\n let storageOrigin: string;\n if (storageEnvOrigin) {\n storageOrigin = storageEnvOrigin;\n if (isLocalOrigin(storageEnvOrigin) && !warning) {\n warning = `BOS_STORAGE_ORIGIN is a local URL (${storageEnvOrigin}) — uploading to a local stack.`;\n }\n } else if (sessionSite) {\n if (isLocalOrigin(sessionSite)) {\n return {\n cdnOrigin,\n storageOrigin: sessionSite,\n apiKey,\n error:\n `The CLI session pins uploads to ${sessionSite} — a local site can never receive ` +\n \"production bundles. Re-run `bos login` against the production site, or set \" +\n \"BOS_STORAGE_ORIGIN explicitly.\",\n };\n }\n storageOrigin = sessionSite;\n } else {\n storageOrigin = `https://${input.gateway}`;\n }\n\n if (!apiKey) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error:\n \"CDN deploy requires bundle-upload credentials — run `bos login --key` and add the printed key \" +\n \"to GitHub secrets as BOS_STORAGE_API_KEY (or run `bos login` for interactive deploys)\",\n };\n }\n if (input.session?.accountId && input.session.accountId !== input.account) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error: `The CLI session was created for ${input.session.accountId}, but the configured account is ${input.account}. Re-run bos login under the matching account.`,\n };\n }\n return { cdnOrigin, storageOrigin, apiKey, warning };\n}\n\n/**\n * Pre-upload probe: the storage origin must actually be the platform API.\n * `/.well-known/mcp.json` is the cheapest unauthenticated platform\n * fingerprint — a random service squatting the port (or a stale session\n * pointing at the wrong server) fails here before megabytes move.\n */\nexport async function probeStorageOrigin(\n storageOrigin: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<string | undefined> {\n const probeUrl = `${storageOrigin.replace(/\\/$/, \"\")}/.well-known/mcp.json`;\n let response: Response;\n try {\n response = await fetchImpl(probeUrl, { headers: { accept: \"application/json\" } });\n } catch (error) {\n return `unreachable (${String(error instanceof Error ? error.message : error)})`;\n }\n if (!response.ok) {\n return `GET /.well-known/mcp.json responded ${response.status}`;\n }\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n return \"did not return the platform MCP descriptor (non-JSON response)\";\n }\n if (!body || typeof body !== \"object\") {\n return \"did not return the platform MCP descriptor\";\n }\n return undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cdn-deploy.cjs","names":[],"sources":["../src/cdn-deploy.ts"],"sourcesContent":["interface SessionCredentialLike {\n apiKey?: string;\n siteUrl?: string;\n accountId?: string | null;\n}\n\nexport interface CdnDeployInputs {\n cdnOrigin: string | undefined;\n storageOrigin: string;\n apiKey: string | undefined;\n warning?: string;\n error?: string;\n}\n\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\", \"0.0.0.0\", \"local\"]);\n\n/** Local/private destinations can never serve production bundles. */\nexport function isLocalOrigin(url: string): boolean {\n try {\n const { hostname, protocol } = new URL(url);\n if (protocol !== \"http:\" && protocol !== \"https:\") return true;\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n return (\n LOCAL_HOSTNAMES.has(host) ||\n host.endsWith(\".localhost\") ||\n host.endsWith(\".local\") ||\n /^127\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^10\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^192\\.168\\.\\d+\\.\\d+$/.test(host) ||\n /^172\\.(1[6-9]|2\\d|3[01])\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"169.254.\") ||\n (host.includes(\":\") &&\n (host.startsWith(\"fe80:\") || host.startsWith(\"fc\") || host.startsWith(\"fd\")))\n );\n } catch {\n return true;\n }\n}\n\n/**\n * Deploy origin resolution (ADR 0020, as amended): the CDN origin comes from\n * the env or the authored `cdn.origin` (inherited via extends) — a hard\n * requirement when uploads are planned, because the host never serves\n * `/bundles` itself. The upload origin is the env, the `bos login` session's\n * siteUrl, or the runtime's own gateway. A session pinned to a local site is\n * a hard error (the origin can only ever be a squatter's port); an env-pinned\n * local origin is deliberate local testing and only warns. Config-only\n * publishes (`uploadsPlanned: false`) ship the committed config verbatim —\n * no origins are resolved and nothing is required.\n */\nexport function resolveCdnDeployInputs(input: {\n env: Record<string, string | undefined>;\n bosConfig: { cdn?: { origin?: string } } | null;\n session: SessionCredentialLike | null;\n account: string;\n gateway: string;\n uploadsPlanned: boolean;\n}): CdnDeployInputs {\n const envCdnOrigin = input.env.BOS_BUNDLE_CDN_ORIGIN?.replace(/\\/$/, \"\");\n const configCdnOrigin = input.bosConfig?.cdn?.origin?.replace(/\\/$/, \"\");\n const storageEnvOrigin = input.env.BOS_STORAGE_ORIGIN?.replace(/\\/$/, \"\");\n const sessionSite = input.session?.siteUrl?.replace(/\\/$/, \"\");\n const apiKey = input.env.BOS_STORAGE_API_KEY ?? input.session?.apiKey;\n const uploadsPlanned = input.uploadsPlanned;\n\n const storageOriginDefault = storageEnvOrigin ?? sessionSite ?? `https://${input.gateway}`;\n\n if (!uploadsPlanned) {\n return { cdnOrigin: undefined, storageOrigin: storageOriginDefault, apiKey };\n }\n\n let cdnOrigin: string | undefined;\n let warning: string | undefined;\n if (envCdnOrigin) {\n cdnOrigin = envCdnOrigin;\n if (isLocalOrigin(envCdnOrigin)) {\n warning = `BOS_BUNDLE_CDN_ORIGIN is a local URL (${envCdnOrigin}) — deploying to a local stack, published bundle URLs will not resolve publicly.`;\n }\n } else if (configCdnOrigin) {\n if (isLocalOrigin(configCdnOrigin)) {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n `the authored config sets cdn.origin to the local URL ${configCdnOrigin} — deploy bundle ` +\n \"origins must be publicly reachable. Set BOS_BUNDLE_CDN_ORIGIN for a local deploy, \" +\n \"or fix cdn.origin (inherited via extends).\",\n };\n }\n cdnOrigin = configCdnOrigin;\n } else {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n \"CDN deploy requires a bundle origin — set `cdn.origin` in bos.config.json \" +\n \"(children inherit it from the base runtime via extends) or BOS_BUNDLE_CDN_ORIGIN \" +\n \"in the environment.\",\n };\n }\n\n let storageOrigin: string;\n if (storageEnvOrigin) {\n storageOrigin = storageEnvOrigin;\n if (isLocalOrigin(storageEnvOrigin) && !warning) {\n warning = `BOS_STORAGE_ORIGIN is a local URL (${storageEnvOrigin}) — uploading to a local stack.`;\n }\n } else if (sessionSite) {\n if (isLocalOrigin(sessionSite)) {\n return {\n cdnOrigin,\n storageOrigin: sessionSite,\n apiKey,\n error:\n `The CLI session pins uploads to ${sessionSite} — a local site can never receive ` +\n \"production bundles. Re-run `bos login` against the production site, or set \" +\n \"BOS_STORAGE_ORIGIN explicitly.\",\n };\n }\n storageOrigin = sessionSite;\n } else {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n warning,\n error:\n `Bundle uploads require an upload-service origin — the gateway (${input.gateway}) no longer serves ` +\n \"uploads (ADR 0027: the upload service is standalone at upload.everything.dev). Set BOS_STORAGE_ORIGIN \" +\n \"in the environment (CI) or re-run `bos login` against the production site so the session pins it.\",\n };\n }\n\n if (!apiKey) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error:\n \"CDN deploy requires bundle-upload credentials — run `bos login --key` and add the printed key \" +\n \"to GitHub secrets as BOS_STORAGE_API_KEY (or run `bos login` for interactive deploys)\",\n };\n }\n if (input.session?.accountId && input.session.accountId !== input.account) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error: `The CLI session was created for ${input.session.accountId}, but the configured account is ${input.account}. Re-run bos login under the matching account.`,\n };\n }\n return { cdnOrigin, storageOrigin, apiKey, warning };\n}\n\n/**\n * Pre-upload probe: the storage origin must actually be the bundle-upload\n * service.\n * `/health` is the primary readiness check, with MCP kept as a compatibility\n * fallback for older fixtures and legacy hosts.\n */\nexport async function probeStorageOrigin(\n storageOrigin: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<string | undefined> {\n const base = storageOrigin.replace(/\\/$/, \"\");\n const healthUrl = `${base}/health`;\n const mcpUrl = `${base}/.well-known/mcp.json`;\n let response: Response;\n try {\n response = await fetchImpl(healthUrl, { headers: { accept: \"text/plain\" } });\n } catch (error) {\n return `unreachable (${String(error instanceof Error ? error.message : error)})`;\n }\n if (response.ok) {\n const body = await response.text().catch(() => \"\");\n if (body.trim() === \"OK\" || body.trim().length === 0) return undefined;\n }\n let mcpResponse: Response;\n try {\n mcpResponse = await fetchImpl(mcpUrl, { headers: { accept: \"application/json\" } });\n } catch {\n return `GET /health responded ${response.status}; fallback /.well-known/mcp.json unreachable`;\n }\n if (!mcpResponse.ok) {\n return `GET /health responded ${response.status}; GET /.well-known/mcp.json responded ${mcpResponse.status}`;\n }\n try {\n const body = await mcpResponse.json();\n if (!body || typeof body !== \"object\") {\n return \"did not return the platform MCP descriptor\";\n }\n return undefined;\n } catch {\n return \"did not return the platform MCP descriptor (non-JSON response)\";\n }\n}\n"],"mappings":"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"}
@@ -73,7 +73,13 @@ function resolveCdnDeployInputs(input) {
73
73
  error: `The CLI session pins uploads to ${sessionSite} — a local site can never receive production bundles. Re-run \`bos login\` against the production site, or set BOS_STORAGE_ORIGIN explicitly.`
74
74
  };
75
75
  storageOrigin = sessionSite;
76
- } else storageOrigin = `https://${input.gateway}`;
76
+ } else return {
77
+ cdnOrigin,
78
+ storageOrigin: storageOriginDefault,
79
+ apiKey,
80
+ warning,
81
+ error: `Bundle uploads require an upload-service origin — the gateway (${input.gateway}) no longer serves uploads (ADR 0027: the upload service is standalone at upload.everything.dev). Set BOS_STORAGE_ORIGIN in the environment (CI) or re-run \`bos login\` against the production site so the session pins it.`
82
+ };
77
83
  if (!apiKey) return {
78
84
  cdnOrigin,
79
85
  storageOrigin,
@@ -96,27 +102,39 @@ function resolveCdnDeployInputs(input) {
96
102
  };
97
103
  }
98
104
  /**
99
- * Pre-upload probe: the storage origin must actually be the platform API.
100
- * `/.well-known/mcp.json` is the cheapest unauthenticated platform
101
- * fingerprint — a random service squatting the port (or a stale session
102
- * pointing at the wrong server) fails here before megabytes move.
105
+ * Pre-upload probe: the storage origin must actually be the bundle-upload
106
+ * service.
107
+ * `/health` is the primary readiness check, with MCP kept as a compatibility
108
+ * fallback for older fixtures and legacy hosts.
103
109
  */
104
110
  async function probeStorageOrigin(storageOrigin, fetchImpl = fetch) {
105
- const probeUrl = `${storageOrigin.replace(/\/$/, "")}/.well-known/mcp.json`;
111
+ const base = storageOrigin.replace(/\/$/, "");
112
+ const healthUrl = `${base}/health`;
113
+ const mcpUrl = `${base}/.well-known/mcp.json`;
106
114
  let response;
107
115
  try {
108
- response = await fetchImpl(probeUrl, { headers: { accept: "application/json" } });
116
+ response = await fetchImpl(healthUrl, { headers: { accept: "text/plain" } });
109
117
  } catch (error) {
110
118
  return `unreachable (${String(error instanceof Error ? error.message : error)})`;
111
119
  }
112
- if (!response.ok) return `GET /.well-known/mcp.json responded ${response.status}`;
113
- let body;
120
+ if (response.ok) {
121
+ const body = await response.text().catch(() => "");
122
+ if (body.trim() === "OK" || body.trim().length === 0) return void 0;
123
+ }
124
+ let mcpResponse;
125
+ try {
126
+ mcpResponse = await fetchImpl(mcpUrl, { headers: { accept: "application/json" } });
127
+ } catch {
128
+ return `GET /health responded ${response.status}; fallback /.well-known/mcp.json unreachable`;
129
+ }
130
+ if (!mcpResponse.ok) return `GET /health responded ${response.status}; GET /.well-known/mcp.json responded ${mcpResponse.status}`;
114
131
  try {
115
- body = await response.json();
132
+ const body = await mcpResponse.json();
133
+ if (!body || typeof body !== "object") return "did not return the platform MCP descriptor";
134
+ return;
116
135
  } catch {
117
136
  return "did not return the platform MCP descriptor (non-JSON response)";
118
137
  }
119
- if (!body || typeof body !== "object") return "did not return the platform MCP descriptor";
120
138
  }
121
139
 
122
140
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"cdn-deploy.mjs","names":[],"sources":["../src/cdn-deploy.ts"],"sourcesContent":["interface SessionCredentialLike {\n apiKey?: string;\n siteUrl?: string;\n accountId?: string | null;\n}\n\nexport interface CdnDeployInputs {\n cdnOrigin: string | undefined;\n storageOrigin: string;\n apiKey: string | undefined;\n warning?: string;\n error?: string;\n}\n\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\", \"0.0.0.0\", \"local\"]);\n\n/** Local/private destinations can never serve production bundles. */\nexport function isLocalOrigin(url: string): boolean {\n try {\n const { hostname, protocol } = new URL(url);\n if (protocol !== \"http:\" && protocol !== \"https:\") return true;\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n return (\n LOCAL_HOSTNAMES.has(host) ||\n host.endsWith(\".localhost\") ||\n host.endsWith(\".local\") ||\n /^127\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^10\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^192\\.168\\.\\d+\\.\\d+$/.test(host) ||\n /^172\\.(1[6-9]|2\\d|3[01])\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"169.254.\") ||\n (host.includes(\":\") &&\n (host.startsWith(\"fe80:\") || host.startsWith(\"fc\") || host.startsWith(\"fd\")))\n );\n } catch {\n return true;\n }\n}\n\n/**\n * Deploy origin resolution (ADR 0020, as amended): the CDN origin comes from\n * the env or the authored `cdn.origin` (inherited via extends) — a hard\n * requirement when uploads are planned, because the host never serves\n * `/bundles` itself. The upload origin is the env, the `bos login` session's\n * siteUrl, or the runtime's own gateway. A session pinned to a local site is\n * a hard error (the origin can only ever be a squatter's port); an env-pinned\n * local origin is deliberate local testing and only warns. Config-only\n * publishes (`uploadsPlanned: false`) ship the committed config verbatim —\n * no origins are resolved and nothing is required.\n */\nexport function resolveCdnDeployInputs(input: {\n env: Record<string, string | undefined>;\n bosConfig: { cdn?: { origin?: string } } | null;\n session: SessionCredentialLike | null;\n account: string;\n gateway: string;\n uploadsPlanned: boolean;\n}): CdnDeployInputs {\n const envCdnOrigin = input.env.BOS_BUNDLE_CDN_ORIGIN?.replace(/\\/$/, \"\");\n const configCdnOrigin = input.bosConfig?.cdn?.origin?.replace(/\\/$/, \"\");\n const storageEnvOrigin = input.env.BOS_STORAGE_ORIGIN?.replace(/\\/$/, \"\");\n const sessionSite = input.session?.siteUrl?.replace(/\\/$/, \"\");\n const apiKey = input.env.BOS_STORAGE_API_KEY ?? input.session?.apiKey;\n const uploadsPlanned = input.uploadsPlanned;\n\n const storageOriginDefault = storageEnvOrigin ?? sessionSite ?? `https://${input.gateway}`;\n\n if (!uploadsPlanned) {\n return { cdnOrigin: undefined, storageOrigin: storageOriginDefault, apiKey };\n }\n\n let cdnOrigin: string | undefined;\n let warning: string | undefined;\n if (envCdnOrigin) {\n cdnOrigin = envCdnOrigin;\n if (isLocalOrigin(envCdnOrigin)) {\n warning = `BOS_BUNDLE_CDN_ORIGIN is a local URL (${envCdnOrigin}) — deploying to a local stack, published bundle URLs will not resolve publicly.`;\n }\n } else if (configCdnOrigin) {\n if (isLocalOrigin(configCdnOrigin)) {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n `the authored config sets cdn.origin to the local URL ${configCdnOrigin} — deploy bundle ` +\n \"origins must be publicly reachable. Set BOS_BUNDLE_CDN_ORIGIN for a local deploy, \" +\n \"or fix cdn.origin (inherited via extends).\",\n };\n }\n cdnOrigin = configCdnOrigin;\n } else {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n \"CDN deploy requires a bundle origin — set `cdn.origin` in bos.config.json \" +\n \"(children inherit it from the base runtime via extends) or BOS_BUNDLE_CDN_ORIGIN \" +\n \"in the environment.\",\n };\n }\n\n let storageOrigin: string;\n if (storageEnvOrigin) {\n storageOrigin = storageEnvOrigin;\n if (isLocalOrigin(storageEnvOrigin) && !warning) {\n warning = `BOS_STORAGE_ORIGIN is a local URL (${storageEnvOrigin}) — uploading to a local stack.`;\n }\n } else if (sessionSite) {\n if (isLocalOrigin(sessionSite)) {\n return {\n cdnOrigin,\n storageOrigin: sessionSite,\n apiKey,\n error:\n `The CLI session pins uploads to ${sessionSite} — a local site can never receive ` +\n \"production bundles. Re-run `bos login` against the production site, or set \" +\n \"BOS_STORAGE_ORIGIN explicitly.\",\n };\n }\n storageOrigin = sessionSite;\n } else {\n storageOrigin = `https://${input.gateway}`;\n }\n\n if (!apiKey) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error:\n \"CDN deploy requires bundle-upload credentials — run `bos login --key` and add the printed key \" +\n \"to GitHub secrets as BOS_STORAGE_API_KEY (or run `bos login` for interactive deploys)\",\n };\n }\n if (input.session?.accountId && input.session.accountId !== input.account) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error: `The CLI session was created for ${input.session.accountId}, but the configured account is ${input.account}. Re-run bos login under the matching account.`,\n };\n }\n return { cdnOrigin, storageOrigin, apiKey, warning };\n}\n\n/**\n * Pre-upload probe: the storage origin must actually be the platform API.\n * `/.well-known/mcp.json` is the cheapest unauthenticated platform\n * fingerprint — a random service squatting the port (or a stale session\n * pointing at the wrong server) fails here before megabytes move.\n */\nexport async function probeStorageOrigin(\n storageOrigin: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<string | undefined> {\n const probeUrl = `${storageOrigin.replace(/\\/$/, \"\")}/.well-known/mcp.json`;\n let response: Response;\n try {\n response = await fetchImpl(probeUrl, { headers: { accept: \"application/json\" } });\n } catch (error) {\n return `unreachable (${String(error instanceof Error ? error.message : error)})`;\n }\n if (!response.ok) {\n return `GET /.well-known/mcp.json responded ${response.status}`;\n }\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n return \"did not return the platform MCP descriptor (non-JSON response)\";\n }\n if (!body || typeof body !== \"object\") {\n return \"did not return the platform MCP descriptor\";\n }\n return undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cdn-deploy.mjs","names":[],"sources":["../src/cdn-deploy.ts"],"sourcesContent":["interface SessionCredentialLike {\n apiKey?: string;\n siteUrl?: string;\n accountId?: string | null;\n}\n\nexport interface CdnDeployInputs {\n cdnOrigin: string | undefined;\n storageOrigin: string;\n apiKey: string | undefined;\n warning?: string;\n error?: string;\n}\n\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\", \"0.0.0.0\", \"local\"]);\n\n/** Local/private destinations can never serve production bundles. */\nexport function isLocalOrigin(url: string): boolean {\n try {\n const { hostname, protocol } = new URL(url);\n if (protocol !== \"http:\" && protocol !== \"https:\") return true;\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n return (\n LOCAL_HOSTNAMES.has(host) ||\n host.endsWith(\".localhost\") ||\n host.endsWith(\".local\") ||\n /^127\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^10\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n /^192\\.168\\.\\d+\\.\\d+$/.test(host) ||\n /^172\\.(1[6-9]|2\\d|3[01])\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"169.254.\") ||\n (host.includes(\":\") &&\n (host.startsWith(\"fe80:\") || host.startsWith(\"fc\") || host.startsWith(\"fd\")))\n );\n } catch {\n return true;\n }\n}\n\n/**\n * Deploy origin resolution (ADR 0020, as amended): the CDN origin comes from\n * the env or the authored `cdn.origin` (inherited via extends) — a hard\n * requirement when uploads are planned, because the host never serves\n * `/bundles` itself. The upload origin is the env, the `bos login` session's\n * siteUrl, or the runtime's own gateway. A session pinned to a local site is\n * a hard error (the origin can only ever be a squatter's port); an env-pinned\n * local origin is deliberate local testing and only warns. Config-only\n * publishes (`uploadsPlanned: false`) ship the committed config verbatim —\n * no origins are resolved and nothing is required.\n */\nexport function resolveCdnDeployInputs(input: {\n env: Record<string, string | undefined>;\n bosConfig: { cdn?: { origin?: string } } | null;\n session: SessionCredentialLike | null;\n account: string;\n gateway: string;\n uploadsPlanned: boolean;\n}): CdnDeployInputs {\n const envCdnOrigin = input.env.BOS_BUNDLE_CDN_ORIGIN?.replace(/\\/$/, \"\");\n const configCdnOrigin = input.bosConfig?.cdn?.origin?.replace(/\\/$/, \"\");\n const storageEnvOrigin = input.env.BOS_STORAGE_ORIGIN?.replace(/\\/$/, \"\");\n const sessionSite = input.session?.siteUrl?.replace(/\\/$/, \"\");\n const apiKey = input.env.BOS_STORAGE_API_KEY ?? input.session?.apiKey;\n const uploadsPlanned = input.uploadsPlanned;\n\n const storageOriginDefault = storageEnvOrigin ?? sessionSite ?? `https://${input.gateway}`;\n\n if (!uploadsPlanned) {\n return { cdnOrigin: undefined, storageOrigin: storageOriginDefault, apiKey };\n }\n\n let cdnOrigin: string | undefined;\n let warning: string | undefined;\n if (envCdnOrigin) {\n cdnOrigin = envCdnOrigin;\n if (isLocalOrigin(envCdnOrigin)) {\n warning = `BOS_BUNDLE_CDN_ORIGIN is a local URL (${envCdnOrigin}) — deploying to a local stack, published bundle URLs will not resolve publicly.`;\n }\n } else if (configCdnOrigin) {\n if (isLocalOrigin(configCdnOrigin)) {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n `the authored config sets cdn.origin to the local URL ${configCdnOrigin} — deploy bundle ` +\n \"origins must be publicly reachable. Set BOS_BUNDLE_CDN_ORIGIN for a local deploy, \" +\n \"or fix cdn.origin (inherited via extends).\",\n };\n }\n cdnOrigin = configCdnOrigin;\n } else {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n error:\n \"CDN deploy requires a bundle origin — set `cdn.origin` in bos.config.json \" +\n \"(children inherit it from the base runtime via extends) or BOS_BUNDLE_CDN_ORIGIN \" +\n \"in the environment.\",\n };\n }\n\n let storageOrigin: string;\n if (storageEnvOrigin) {\n storageOrigin = storageEnvOrigin;\n if (isLocalOrigin(storageEnvOrigin) && !warning) {\n warning = `BOS_STORAGE_ORIGIN is a local URL (${storageEnvOrigin}) — uploading to a local stack.`;\n }\n } else if (sessionSite) {\n if (isLocalOrigin(sessionSite)) {\n return {\n cdnOrigin,\n storageOrigin: sessionSite,\n apiKey,\n error:\n `The CLI session pins uploads to ${sessionSite} — a local site can never receive ` +\n \"production bundles. Re-run `bos login` against the production site, or set \" +\n \"BOS_STORAGE_ORIGIN explicitly.\",\n };\n }\n storageOrigin = sessionSite;\n } else {\n return {\n cdnOrigin,\n storageOrigin: storageOriginDefault,\n apiKey,\n warning,\n error:\n `Bundle uploads require an upload-service origin — the gateway (${input.gateway}) no longer serves ` +\n \"uploads (ADR 0027: the upload service is standalone at upload.everything.dev). Set BOS_STORAGE_ORIGIN \" +\n \"in the environment (CI) or re-run `bos login` against the production site so the session pins it.\",\n };\n }\n\n if (!apiKey) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error:\n \"CDN deploy requires bundle-upload credentials — run `bos login --key` and add the printed key \" +\n \"to GitHub secrets as BOS_STORAGE_API_KEY (or run `bos login` for interactive deploys)\",\n };\n }\n if (input.session?.accountId && input.session.accountId !== input.account) {\n return {\n cdnOrigin,\n storageOrigin,\n apiKey,\n warning,\n error: `The CLI session was created for ${input.session.accountId}, but the configured account is ${input.account}. Re-run bos login under the matching account.`,\n };\n }\n return { cdnOrigin, storageOrigin, apiKey, warning };\n}\n\n/**\n * Pre-upload probe: the storage origin must actually be the bundle-upload\n * service.\n * `/health` is the primary readiness check, with MCP kept as a compatibility\n * fallback for older fixtures and legacy hosts.\n */\nexport async function probeStorageOrigin(\n storageOrigin: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<string | undefined> {\n const base = storageOrigin.replace(/\\/$/, \"\");\n const healthUrl = `${base}/health`;\n const mcpUrl = `${base}/.well-known/mcp.json`;\n let response: Response;\n try {\n response = await fetchImpl(healthUrl, { headers: { accept: \"text/plain\" } });\n } catch (error) {\n return `unreachable (${String(error instanceof Error ? error.message : error)})`;\n }\n if (response.ok) {\n const body = await response.text().catch(() => \"\");\n if (body.trim() === \"OK\" || body.trim().length === 0) return undefined;\n }\n let mcpResponse: Response;\n try {\n mcpResponse = await fetchImpl(mcpUrl, { headers: { accept: \"application/json\" } });\n } catch {\n return `GET /health responded ${response.status}; fallback /.well-known/mcp.json unreachable`;\n }\n if (!mcpResponse.ok) {\n return `GET /health responded ${response.status}; GET /.well-known/mcp.json responded ${mcpResponse.status}`;\n }\n try {\n const body = await mcpResponse.json();\n if (!body || typeof body !== \"object\") {\n return \"did not return the platform MCP descriptor\";\n }\n return undefined;\n } catch {\n return \"did not return the platform MCP descriptor (non-JSON response)\";\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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"}
package/dist/cli/init.cjs CHANGED
@@ -107,8 +107,6 @@ const INIT_ROOT_PATTERNS = [
107
107
  ".env.example",
108
108
  ".gitignore",
109
109
  "biome.json",
110
- "railway.json",
111
- "railway.toml",
112
110
  "AGENTS.md",
113
111
  ".agents/skills/**",
114
112
  "skills-lock.json",