UNPKG

everything-dev

Version:

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

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