UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

1 lines • 17.5 kB
{"version":3,"file":"secrets.mjs","names":[],"sources":["../../src/handlers/secrets.ts"],"sourcesContent":["import process from \"node:process\";\nimport * as p from \"@clack/prompts\";\nimport { Effect } from \"effect\";\nimport type { ResultRenderer } from \"../cli/commands/context\";\nimport type { SecretsListResult, SecretsRemoveResult, SecretsSetResult } from \"../contract\";\nimport { SessionStore } from \"../services/auth/session-store\";\nimport { parseSiteErrorDetail, SiteClient } from \"../services/auth/site-client\";\nimport { isReservedSecretName, isValidSecretName } from \"../services/hosting/secrets\";\nimport { ResolutionSessionService } from \"../services/resolution/session\";\nimport { colors, icons } from \"../utils/theme\";\nimport type { BosBuilder } from \"./shared\";\n\n/**\n * Resolve the runtime identity + declared secrets from the resolution\n * session's config (the authored bos.app.ts in the cwd). The rows the vault\n * stores are keyed by this identity; the vault ITSELF is whichever deployment\n * answers at the credential's site.\n */\nfunction collectDeclaredSecrets(config: unknown): string[] {\n const cfg = config as {\n secrets?: string[];\n app?: Record<string, { secrets?: string[] } | undefined>;\n plugins?: Record<string, { secrets?: string[] } | undefined> | undefined;\n } | null;\n if (!cfg) return [];\n const declared = new Set<string>();\n for (const secret of cfg.secrets ?? []) declared.add(secret);\n for (const section of Object.values(cfg.app ?? {})) {\n for (const secret of section?.secrets ?? []) declared.add(secret);\n }\n for (const plugin of Object.values(cfg.plugins ?? {})) {\n for (const secret of plugin?.secrets ?? []) declared.add(secret);\n }\n return [...declared];\n}\n\n/**\n * v1 tier heuristic (plan 055 ticket 09): authoritative tier data arrives with\n * the tenant bindings surface (ticket 14). Until then, a target that is\n * neither the declared config's own runtime nor a child of the vault site's\n * runtime gets an honest warning — a BYO-account runtime supplies its own env,\n * so rows stored for it would never be delivered.\n */\nfunction hostingRelationshipWarning(\n config: unknown,\n siteUrl: string,\n account: string,\n gateway: string,\n): string | null {\n const cfg = config as { account?: string; domain?: string; extends?: unknown } | null | undefined;\n if (cfg?.account === account && cfg?.domain === gateway) return null;\n const extendsText = JSON.stringify(cfg?.extends ?? \"\");\n const siteHost = new URL(siteUrl).host;\n if (extendsText.includes(siteHost) || extendsText.includes(new URL(siteUrl).hostname)) {\n return null;\n }\n return (\n `The target runtime (${account}/${gateway}) is not this config's declared runtime, and its ` +\n `extends chain does not reference the vault site (${siteHost}). If this is a BYO-account ` +\n `runtime, it supplies its own env — these rows will not be delivered to it.`\n );\n}\n\nexport function registerSecrets(builder: BosBuilder) {\n return {\n secretsSet: builder.secretsSet.effect(function* (_opts, input) {\n const session = yield* ResolutionSessionService;\n const sessions = yield* SessionStore;\n const siteClient = yield* SiteClient;\n\n const configDir = session?.root ?? process.cwd();\n const credential = sessions.read(configDir)?.credential;\n if (!credential?.apiKey) {\n return { status: \"error\" as const, error: \"No CLI credential — run `bos login` first\" };\n }\n const siteUrl = input.site ?? credential.siteUrl;\n if (!siteUrl) {\n return {\n status: \"error\" as const,\n error: \"No vault site — pass --site or re-run `bos login`\",\n };\n }\n\n if (!isValidSecretName(input.name)) {\n return {\n status: \"error\" as const,\n error: isReservedSecretName(input.name)\n ? `Secret name ${input.name} is reserved (BOS_* and PORT are composed by the platform)`\n : \"Secret name must be UPPER_SNAKE_CASE env-var style\",\n };\n }\n\n const value =\n input.value ??\n (yield* Effect.promise(() =>\n p.password({ message: `Value for ${input.name} (input hidden)` }),\n ));\n if (p.isCancel(value)) process.exit(0);\n if (!value) {\n return { status: \"error\" as const, error: \"Empty secret value — nothing stored\" };\n }\n\n const account = session?.config?.account;\n const gateway = session?.config?.domain;\n if (!account || !gateway) {\n return {\n status: \"error\" as const,\n error: \"No declared runtime identity — the authored config's account/domain are required\",\n };\n }\n\n const response = yield* Effect.promise(() =>\n siteClient.rpcPost(\n siteUrl,\n \"/api/rpc/secrets/set\",\n { account, gateway, name: input.name, value },\n undefined,\n { \"x-api-key\": credential.apiKey },\n ),\n );\n if (response.status !== 200) {\n const detail = parseSiteErrorDetail(response.status, response.json);\n return { status: \"error\" as const, error: detail ?? `Vault refused (${response.status})` };\n }\n return {\n status: \"set\" as const,\n name: input.name,\n siteUrl,\n warning:\n hostingRelationshipWarning(session?.config, siteUrl, account, gateway) ?? undefined,\n } satisfies SecretsSetResult;\n }),\n\n secretsList: builder.secretsList.effect(function* (_opts, input) {\n const session = yield* ResolutionSessionService;\n const sessions = yield* SessionStore;\n const siteClient = yield* SiteClient;\n\n const configDir = session?.root ?? process.cwd();\n const credential = sessions.read(configDir)?.credential;\n if (!credential?.apiKey) {\n return { status: \"error\" as const, error: \"No CLI credential — run `bos login` first\" };\n }\n const siteUrl = input.site ?? credential.siteUrl;\n if (!siteUrl) {\n return {\n status: \"error\" as const,\n error: \"No vault site — pass --site or re-run `bos login`\",\n };\n }\n\n const account = input.account ?? session?.config?.account;\n const gateway = input.gateway ?? session?.config?.domain;\n if (!account || !gateway) {\n return {\n status: \"error\" as const,\n error: \"No target runtime — pass --account/--gateway or run from the runtime's repo\",\n };\n }\n\n const response = yield* Effect.promise(() =>\n siteClient.rpcPost(\n siteUrl,\n \"/api/rpc/secrets/list\",\n { account, gateway, declared: collectDeclaredSecrets(session?.config) },\n undefined,\n { \"x-api-key\": credential.apiKey },\n ),\n );\n if (response.status !== 200) {\n const detail = parseSiteErrorDetail(response.status, response.json);\n return { status: \"error\" as const, error: detail ?? `Vault refused (${response.status})` };\n }\n const body = (\n response.json as { json?: { secrets?: SecretsListResult[\"secrets\"]; missing?: string[] } }\n ).json;\n return {\n status: \"listed\" as const,\n secrets: body?.secrets ?? [],\n missing: body?.missing ?? [],\n siteUrl,\n warning:\n hostingRelationshipWarning(session?.config, siteUrl, account, gateway) ?? undefined,\n } satisfies SecretsListResult;\n }),\n\n secretsRemove: builder.secretsRemove.effect(function* (_opts, input) {\n const session = yield* ResolutionSessionService;\n const sessions = yield* SessionStore;\n const siteClient = yield* SiteClient;\n\n const configDir = session?.root ?? process.cwd();\n const credential = sessions.read(configDir)?.credential;\n if (!credential?.apiKey) {\n return { status: \"error\" as const, error: \"No CLI credential — run `bos login` first\" };\n }\n const siteUrl = credential.siteUrl;\n if (!siteUrl) {\n return { status: \"error\" as const, error: \"No vault site — re-run `bos login`\" };\n }\n\n if (!input.yes) {\n const confirmed = yield* Effect.promise(() =>\n p.confirm({ message: `Remove ${input.name} from the vault at ${siteUrl}?` }),\n );\n if (p.isCancel(confirmed)) process.exit(0);\n if (!confirmed) {\n return {\n status: \"error\" as const,\n name: input.name,\n error: \"Cancelled — nothing removed\",\n };\n }\n }\n\n const account = session?.config?.account;\n const gateway = session?.config?.domain;\n if (!account || !gateway) {\n return {\n status: \"error\" as const,\n error: \"No declared runtime identity — the authored config's account/domain are required\",\n };\n }\n\n const response = yield* Effect.promise(() =>\n siteClient.rpcPost(\n siteUrl,\n \"/api/rpc/secrets/remove\",\n { account, gateway, name: input.name },\n undefined,\n { \"x-api-key\": credential.apiKey },\n ),\n );\n if (response.status !== 200) {\n const detail = parseSiteErrorDetail(response.status, response.json);\n return { status: \"error\" as const, error: detail ?? `Vault refused (${response.status})` };\n }\n const body = (response.json as { json?: { removed?: boolean } }).json;\n return body?.removed\n ? ({ status: \"removed\" as const, name: input.name } satisfies SecretsRemoveResult)\n : ({ status: \"not-found\" as const, name: input.name } satisfies SecretsRemoveResult);\n }),\n };\n}\n\nexport const secretsRenderers: Record<string, ResultRenderer> = {\n secretsSet: async (result: SecretsSetResult) => {\n console.log();\n if (result.status === \"set\") {\n console.log(` ${colors.green(`${icons.ok} Secret ${result.name} stored`)}`);\n console.log(` ${colors.dim(`Vault: ${result.siteUrl}`)}`);\n console.log(` ${colors.dim(\"The value is sealed at rest and never echoed back\")}`);\n if (result.warning) console.log(` ${colors.yellow(\"⚠\")} ${result.warning}`);\n } else {\n console.log(` ${colors.error(`${icons.err} ${result.error ?? \"Failed to store secret\"}`)}`);\n }\n console.log();\n },\n\n secretsList: async (result: SecretsListResult) => {\n console.log();\n if (result.status !== \"listed\") {\n console.log(` ${colors.error(`${icons.err} ${result.error ?? \"Failed to list secrets\"}`)}`);\n console.log();\n return;\n }\n console.log(\n ` ${colors.green(`${icons.ok} Secrets`)} ${colors.dim(`vault: ${result.siteUrl}`)}`,\n );\n if (result.warning) console.log(` ${colors.yellow(\"⚠\")} ${result.warning}`);\n const rows = result.secrets ?? [];\n if (rows.length === 0) {\n console.log(` ${colors.dim(\"No secrets stored and nothing declared\")}`);\n }\n for (const secret of rows) {\n const mark =\n secret.state === \"set\"\n ? colors.green(\"set\")\n : secret.state === \"missing\"\n ? colors.yellow(\"missing\")\n : colors.dim(\"undeclared\");\n console.log(` ${colors.dim(secret.name.padEnd(24))} ${mark}`);\n }\n if ((result.missing ?? []).length > 0) {\n console.log();\n console.log(\n ` ${colors.yellow(\"⚠\")} Missing: ${result.missing!.join(\", \")} — set them with \\`bos secrets set NAME\\``,\n );\n }\n console.log();\n },\n\n secretsRemove: async (result: SecretsRemoveResult) => {\n console.log();\n if (result.status === \"removed\") {\n console.log(` ${colors.green(`${icons.ok} Removed ${result.name}`)}`);\n } else if (result.status === \"not-found\") {\n console.log(` ${colors.yellow(\"⚠\")} ${result.name} was not stored`);\n } else {\n console.log(` ${colors.error(`${icons.err} ${result.error ?? \"Failed to remove\"}`)}`);\n }\n console.log();\n },\n};\n"],"mappings":"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"}