@cosmicdrift/kumiko-server-runtime 0.387.0 → 0.389.0

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 (61) hide show
  1. package/dist/boot/boot-crypto.js +3 -3
  2. package/dist/boot/shared-boot-steps.d.ts +46 -0
  3. package/dist/boot/shared-boot-steps.js +160 -0
  4. package/dist/boot/wait-for-database.d.ts +5 -0
  5. package/dist/boot/wait-for-database.js +7 -6
  6. package/dist/boot/worker-boot-core.js +24 -38
  7. package/dist/boot-options/billing-option.d.ts +18 -9
  8. package/dist/boot-options/billing-option.js +44 -18
  9. package/dist/boot-options/demo-mode.d.ts +3 -3
  10. package/dist/boot-options/demo-mode.js +4 -1
  11. package/dist/boot-options/file-storage-option.js +6 -6
  12. package/dist/boot-options/option-env-schema.js +11 -2
  13. package/dist/boot-options/resolve-boot-options.d.ts +1 -0
  14. package/dist/boot-options/resolve-boot-options.js +11 -5
  15. package/dist/boot-options/stack-boot-requirements.d.ts +1 -1
  16. package/dist/boot-options/stack-boot-requirements.js +2 -14
  17. package/dist/build-prod-bundle.js +13 -13
  18. package/dist/bundled-lists-table.d.ts +2 -0
  19. package/dist/bundled-lists-table.js +56 -0
  20. package/dist/compose-auth-options.d.ts +16 -0
  21. package/dist/compose-auth-options.js +49 -0
  22. package/dist/compose-features.d.ts +5 -17
  23. package/dist/compose-features.js +6 -53
  24. package/dist/csp-hashes.js +1 -1
  25. package/dist/extra-routes-deps.d.ts +4 -0
  26. package/dist/extra-routes-deps.js +1 -0
  27. package/dist/fatal-error-handlers.js +7 -0
  28. package/dist/files-for-tenant.d.ts +3 -0
  29. package/dist/files-for-tenant.js +10 -0
  30. package/dist/renderer-web-fonts.js +3 -9
  31. package/dist/require-env.d.ts +2 -0
  32. package/dist/require-env.js +21 -0
  33. package/dist/resolve-installed-package.d.ts +10 -0
  34. package/dist/resolve-installed-package.js +32 -0
  35. package/dist/resolve-tailwind-cli.d.ts +3 -8
  36. package/dist/resolve-tailwind-cli.js +8 -25
  37. package/dist/run-bootstrap.d.ts +1 -1
  38. package/dist/run-bootstrap.js +1 -1
  39. package/dist/run-prod-app-auth.d.ts +17 -0
  40. package/dist/run-prod-app-auth.js +83 -0
  41. package/dist/run-prod-app-boot-context.d.ts +2 -0
  42. package/dist/run-prod-app-boot-context.js +15 -7
  43. package/dist/run-prod-app-config.d.ts +36 -0
  44. package/dist/run-prod-app-config.js +169 -0
  45. package/dist/run-prod-app-connections.d.ts +17 -0
  46. package/dist/run-prod-app-connections.js +59 -0
  47. package/dist/run-prod-app-entrypoint.d.ts +14 -0
  48. package/dist/run-prod-app-entrypoint.js +149 -0
  49. package/dist/run-prod-app-handle.d.ts +20 -0
  50. package/dist/run-prod-app-handle.js +105 -0
  51. package/dist/run-prod-app-request-context.d.ts +15 -0
  52. package/dist/run-prod-app-request-context.js +103 -0
  53. package/dist/run-prod-app-seeds.d.ts +13 -0
  54. package/dist/run-prod-app-seeds.js +88 -0
  55. package/dist/run-prod-app-static-files.js +9 -1
  56. package/dist/run-prod-app.d.ts +27 -19
  57. package/dist/run-prod-app.js +47 -727
  58. package/dist/run-worker-app.d.ts +12 -2
  59. package/dist/run-worker-app.js +2 -0
  60. package/package.json +27 -8
  61. package/src/changes.json +86 -0
@@ -1,34 +1,17 @@
1
1
  // Resolves the locally installed @tailwindcss/cli binary to its absolute path.
2
2
  // We avoid `bunx` because it still requests the registry manifest for a locally
3
3
  // installed package and fails with `FailedToOpenSocket` when offline.
4
- //
5
- // Kept separate from create-kumiko-server.ts so the Bun and resolution-failure
6
- // branches can be tested under Vitest/Node without booting the server.
7
- import { dirname, resolve } from "node:path";
4
+ import { dirname, join } from "node:path";
5
+ import { findInstalledPackageDir } from "./resolve-installed-package.js";
8
6
  export function resolveTailwindCli(deps) {
9
7
  if (deps.bun === undefined)
10
8
  return undefined;
11
- try {
12
- const pkgJsonPath = deps.bun.resolveSync("@tailwindcss/cli/package.json", deps.cwd);
13
- // bin: { tailwindcss: "./dist/index.mjs" } → absoluter Pfad
14
- return resolve(pkgJsonPath, "..", "dist", "index.mjs");
15
- }
16
- catch {
17
- return undefined;
18
- }
9
+ const cliDir = findInstalledPackageDir("@tailwindcss/cli", deps.cwd);
10
+ return cliDir === undefined ? undefined : join(cliDir, "dist", "index.mjs");
19
11
  }
20
- /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
21
- * CSS file. Skip renderer-web fallback (or fail build) when peer deps
22
- * aren't resolvable from that directory — e.g. Bun cache copies of
23
- * `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
12
+ /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry CSS file, so
13
+ * tailwindcss must be installed above it (not just next to a Bun cache copy of renderer-web). */
24
14
  export function canResolveTailwindStylesheet(entryCss, deps) {
25
- if (deps.bun === undefined)
26
- return false;
27
- try {
28
- deps.bun.resolveSync("tailwindcss", dirname(entryCss));
29
- return true;
30
- }
31
- catch {
32
- return false;
33
- }
15
+ return (deps.bun !== undefined &&
16
+ findInstalledPackageDir("tailwindcss", dirname(entryCss)) !== undefined);
34
17
  }
@@ -1,5 +1,5 @@
1
1
  import { type BootstrapPlan, type BootstrapReport } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
- import { type RunProdAppAuthOptions } from "./run-prod-app.ts";
2
+ import type { RunProdAppAuthOptions } from "./run-prod-app.ts";
3
3
  import type { RunWorkerAppOptions } from "./run-worker-app.ts";
4
4
  export type RunBootstrapOptions = Omit<RunWorkerAppOptions, "wireComponents" | "includeBundled" | "jobs" | "eventDispatcher" | "metrics"> & BootstrapPlan & {
5
5
  /** The same auth block the app passes to runProdApp (`admin` is ignored).
@@ -12,7 +12,7 @@
12
12
  import { bootstrapTenants, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
13
13
  import { bootWorkerProcess, resolveWorkerEnvSource } from "./boot/worker-boot-core.js";
14
14
  import { buildComposeAuthOptions } from "./compose-features.js";
15
- import { requireEnv } from "./run-prod-app.js";
15
+ import { requireEnv } from "./require-env.js";
16
16
  import { resolveAuthMail } from "./run-prod-app-boot-context.js";
17
17
  const PROCESS_NAME = "runBootstrap";
18
18
  function maskEmailForLog(email) {
@@ -0,0 +1,17 @@
1
+ import { type AuthRoutesConfig, type LoginRateLimiter, type TokenVerifier } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { buildProdSessionAuth } from "./run-prod-app-boot-context.ts";
4
+ import type { ProdBootConfig, ProdComposedFeatures } from "./run-prod-app-config.ts";
5
+ import type { ProdConnections } from "./run-prod-app-connections.ts";
6
+ type PatAuthFragment = {
7
+ readonly tokenVerifier: TokenVerifier;
8
+ readonly patRateLimiter: LoginRateLimiter;
9
+ };
10
+ export type ProdAuthWiring = {
11
+ readonly mfaFeature: FeatureDefinition | undefined;
12
+ readonly sessionAuthFragment: Awaited<ReturnType<typeof buildProdSessionAuth>> | undefined;
13
+ readonly patAuthFragment: PatAuthFragment | undefined;
14
+ };
15
+ export declare function wireProdAuth(config: ProdBootConfig, composed: ProdComposedFeatures, connections: ProdConnections): Promise<ProdAuthWiring>;
16
+ export declare function buildProdAuthRoutes(effectiveAuth: NonNullable<ProdComposedFeatures["effectiveAuth"]>, config: ProdBootConfig, composed: ProdComposedFeatures, connections: ProdConnections, authWiring: ProdAuthWiring): AuthRoutesConfig;
17
+ export {};
@@ -0,0 +1,83 @@
1
+ import { EXT_SESSION_STORE, EXT_TOKEN_VERIFIER, resolveTokenVerifier, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
+ import { AUTH_MFA_FEATURE, bindMfaTokenSecretsFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
3
+ import { PAT_FEATURE, patRateLimitFromFeature, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
4
+ import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
5
+ import { createRedisLoginRateLimiter, } from "@cosmicdrift/kumiko-framework/api";
6
+ import { assertMfaSwitchTenantGate, buildSharedAuthRoutes } from "./boot/shared-boot-steps.js";
7
+ import { buildProdSessionAuth, wireProdPatAutoRevoke } from "./run-prod-app-boot-context.js";
8
+ import { shouldWireProdSessions } from "./session-wiring.js";
9
+ // Sessions are wired directly because db has already been created; unlike
10
+ // runDevApp, no late binding is needed. Once a sessionChecker is wired, JWTs
11
+ // without sid are rejected, so production sessions cannot be bypassed.
12
+ // sessionMassRevoker (the fourth callback from createSessionCallbacks) is not
13
+ // part of AuthRoutesConfig; bindAutoRevokeFromFeature hands it to the sessions
14
+ // feature so password change and reset revoke all sessions. Secure by default: the sessionStore provider
15
+ // goes through resolveSessionStore.
16
+ export async function wireProdAuth(config, composed, connections) {
17
+ const { features, registry, effectiveAuth, sessionsFeature } = composed;
18
+ const { db, redis } = connections;
19
+ const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
20
+ if (mfaFeature) {
21
+ bindMfaTokenSecretsFromFeature(mfaFeature)?.(config.jwtSecret, config.envSource);
22
+ assertMfaSwitchTenantGate(registry, "runProdApp");
23
+ }
24
+ // Hoisted above buildProdSessionAuth: that call also wires MFA-enable/
25
+ // disable → PAT-revoke (bindRevokeAllPatTokensFromFeature), so it needs
26
+ // patFeature too.
27
+ const patFeature = features.find((f) => f.name === PAT_FEATURE);
28
+ const sessionAuthFragment = shouldWireProdSessions(Boolean(effectiveAuth), registry.getExtensionUsages(EXT_SESSION_STORE).length > 0)
29
+ ? await buildProdSessionAuth(db, registry, sessionsFeature, mfaFeature, patFeature)
30
+ : undefined;
31
+ // Password-change → PAT-revoke: independent of sessions/MFA being mounted
32
+ // (buildProdSessionAuth only runs when a sessionStore provider is
33
+ // registered), so wired unconditionally here whenever PAT itself is.
34
+ if (patFeature) {
35
+ wireProdPatAutoRevoke(db, patFeature);
36
+ }
37
+ // Token-verifier opt-in: any provider feature (personal-access-tokens, a
38
+ // future auth-provider-jwt, ...) self-registers via
39
+ // r.useExtension(EXT_TOKEN_VERIFIER, ...) — wire one generic resolver
40
+ // whenever at least one is mounted, resolved by shape at request-time.
41
+ // PAT keeps its own per-token rate limiter (patRateLimiter), unrelated to
42
+ // verification.
43
+ const hasTokenVerifierProviders = registry.getExtensionUsages(EXT_TOKEN_VERIFIER).length > 0;
44
+ let patAuthFragment;
45
+ if (effectiveAuth && patFeature && hasTokenVerifierProviders) {
46
+ const rl = patRateLimitFromFeature(patFeature);
47
+ patAuthFragment = {
48
+ tokenVerifier: (rawToken) => resolveTokenVerifier({ db, registry }, rawToken),
49
+ patRateLimiter: createRedisLoginRateLimiter(redis, rl.maxRequests, rl.windowMs, "pat"),
50
+ };
51
+ }
52
+ return { mfaFeature, sessionAuthFragment, patAuthFragment };
53
+ }
54
+ function buildMfaRateLimits(redis) {
55
+ return {
56
+ mfaVerifyRateLimit: createRedisLoginRateLimiter(redis, undefined, undefined, "mfa-verify"),
57
+ mfaPreauthEnableStartRateLimit: createRedisLoginRateLimiter(redis, undefined, undefined, "mfa-preauth-start"),
58
+ mfaPreauthConfirmRateLimit: createRedisLoginRateLimiter(redis, undefined, undefined, "mfa-preauth-confirm"),
59
+ };
60
+ }
61
+ export function buildProdAuthRoutes(effectiveAuth, config, composed, connections, authWiring) {
62
+ const { redis } = connections;
63
+ const { authPublicOrigin } = composed;
64
+ const { trustedProxyHops } = config;
65
+ return {
66
+ ...buildSharedAuthRoutes(effectiveAuth, Boolean(authWiring.mfaFeature)),
67
+ userQuery: UserQueries.findForAuth,
68
+ // Redis-backed, not the in-memory default createAuthRoutes falls
69
+ // back to — an in-process limiter only rate-limits within a single
70
+ // replica, so a multi-instance prod deployment would silently give
71
+ // each replica its own bucket. Redis is required infra
72
+ // here already (REDIS_URL), so this is free.
73
+ loginRateLimit: createRedisLoginRateLimiter(redis),
74
+ ...(effectiveAuth.secureCookies !== undefined && {
75
+ secureCookies: effectiveAuth.secureCookies,
76
+ }),
77
+ ...(authPublicOrigin !== undefined && { publicOrigin: authPublicOrigin }),
78
+ ...(trustedProxyHops !== undefined && { trustedProxyHops }),
79
+ ...authWiring.sessionAuthFragment,
80
+ ...authWiring.patAuthFragment,
81
+ ...(authWiring.mfaFeature && buildMfaRateLimits(redis)),
82
+ };
83
+ }
@@ -16,6 +16,8 @@ export declare function addConfigAccessorFactory<T extends {
16
16
  export declare function loggerFromExtraContext(extraContext: Record<string, unknown>): Logger | undefined;
17
17
  /** Config key (`feature:config:name`) to value; wins over the key's env variable. */
18
18
  export type ConfigOverridesInput = Readonly<Record<string, string | number | boolean>>;
19
+ /** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
20
+ export declare function buildConfigOverrides(registry: Registry, envSource: Record<string, string | undefined>, appOverrides: ConfigOverridesInput | undefined): import("@cosmicdrift/kumiko-bundled-features/config").AppConfigOverrides;
19
21
  type BootExtraContextOptions = {
20
22
  readonly db: DbConnection;
21
23
  readonly features: readonly FeatureDefinition[];
@@ -120,12 +120,16 @@ function bootCryptoContext(crypto) {
120
120
  ...(crypto.configCipher && { configEncryption: crypto.configCipher }),
121
121
  };
122
122
  }
123
- function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
123
+ /** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
124
+ export function buildConfigOverrides(registry, envSource, appOverrides) {
124
125
  const envOverrides = buildEnvConfigOverrides(registry, envSource);
126
+ return appOverrides
127
+ ? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
128
+ : envOverrides;
129
+ }
130
+ function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
125
131
  return createConfigResolver({
126
- appOverrides: appOverrides
127
- ? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
128
- : envOverrides,
132
+ appOverrides: buildConfigOverrides(registry, envSource, appOverrides),
129
133
  ...(crypto.configCipher && { cipher: crypto.configCipher }),
130
134
  });
131
135
  }
@@ -133,9 +137,13 @@ function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
133
137
  // layouts), so apps without auth still get a resolver for it; the ctx entry
134
138
  // stays auth-only.
135
139
  function resolveDeliveryConfigResolver(opts, hasDeliveryFeature, crypto) {
136
- return opts.hasAuth || hasDeliveryFeature
137
- ? createBootConfigResolver(opts.registry, opts.envSource, crypto, opts.appOverrides)
138
- : undefined;
140
+ if (opts.hasAuth || hasDeliveryFeature) {
141
+ return createBootConfigResolver(opts.registry, opts.envSource, crypto, opts.appOverrides);
142
+ }
143
+ if (opts.appOverrides) {
144
+ throw new Error("[runProdApp] appOverrides needs `auth` or a delivery feature: without one no config resolver is built and the overrides would be ignored.");
145
+ }
146
+ return undefined;
139
147
  }
140
148
  function bootDeliveryContext(opts, parts) {
141
149
  if (!opts.features.some((f) => f.name === DELIVERY_FEATURE))
@@ -0,0 +1,36 @@
1
+ import { loadJwtSecretOrKeyring } from "@cosmicdrift/kumiko-framework/api";
2
+ import { type FeatureDefinition, type Registry } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
4
+ import { resolveBootCrypto } from "./boot/boot-crypto.ts";
5
+ import { type BootOptionWiring } from "./boot-options/resolve-boot-options.ts";
6
+ import { resolveRateLimit } from "./rate-limit-defaults.ts";
7
+ import { resolveUnsubscribeWiring } from "./resolve-unsubscribe-wiring.ts";
8
+ import type { RunProdAppOptions } from "./run-prod-app.ts";
9
+ import { resolveAuthMail } from "./run-prod-app-boot-context.ts";
10
+ export type BootAbortCleanup = () => void | Promise<void>;
11
+ export type RegisterBootAbortCleanup = (cleanup: BootAbortCleanup) => void;
12
+ export declare function runWithBootAbortCleanups<T>(boot: (register: RegisterBootAbortCleanup) => Promise<T>): Promise<T>;
13
+ export type ProdBootConfig = {
14
+ readonly bootOptionWiring: BootOptionWiring;
15
+ readonly envSource: Record<string, string | undefined>;
16
+ readonly bootKms: BootOptionWiring["kms"];
17
+ readonly databaseUrl: string;
18
+ readonly redisUrl: string;
19
+ readonly jwtSecret: string;
20
+ readonly jwtSecretOrKeyring: ReturnType<typeof loadJwtSecretOrKeyring>;
21
+ readonly jwtIssuer: string | undefined;
22
+ readonly instanceId: string | undefined;
23
+ readonly trustedProxyHops: number | undefined;
24
+ readonly resolvedRateLimit: ReturnType<typeof resolveRateLimit>;
25
+ readonly port: number;
26
+ };
27
+ export type ProdComposedFeatures = ReturnType<typeof resolveUnsubscribeWiring> & {
28
+ readonly effectiveAuth: ReturnType<typeof resolveAuthMail<NonNullable<RunProdAppOptions["auth"]>>> | undefined;
29
+ readonly authPublicOrigin: string | undefined;
30
+ readonly features: readonly FeatureDefinition[];
31
+ readonly bootCrypto: ReturnType<typeof resolveBootCrypto>;
32
+ readonly sessionsFeature: FeatureDefinition | undefined;
33
+ readonly registry: Registry;
34
+ };
35
+ export declare function resolveProdBootConfig(options: RunProdAppOptions, decryptedEnvSource: Record<string, string | undefined>, log: Logger, registerAbortCleanup: RegisterBootAbortCleanup): Promise<ProdBootConfig>;
36
+ export declare function composeProdFeatures(options: RunProdAppOptions, config: ProdBootConfig, log: Logger): ProdComposedFeatures;
@@ -0,0 +1,169 @@
1
+ import { EXT_SESSION_STORE } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
+ import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
3
+ import { loadJwtSecretOrKeyring, TRUSTED_PROXY_HOPS_ENV } from "@cosmicdrift/kumiko-framework/api";
4
+ import { createRegistry, } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
6
+ import { resolveBootCrypto } from "./boot/boot-crypto.js";
7
+ import { composeAppFeatures, validateComposedFeatures, withEnvDefaults, } from "./boot/shared-boot-steps.js";
8
+ import { resolveBootOptionWiring, } from "./boot-options/resolve-boot-options.js";
9
+ import { assertStackBootRequirements } from "./boot-options/stack-boot-requirements.js";
10
+ import { assertPiiBootInvariants } from "./pii-boot-gate.js";
11
+ import { resolveRateLimit } from "./rate-limit-defaults.js";
12
+ import { readEnv, requireEnv } from "./require-env.js";
13
+ import { resolveAuthPublicOrigin } from "./resolve-auth-public-origin.js";
14
+ import { resolveUnsubscribeWiring } from "./resolve-unsubscribe-wiring.js";
15
+ import { resolveAuthMail } from "./run-prod-app-boot-context.js";
16
+ import { assertSessionBootInvariants } from "./session-boot-gate.js";
17
+ import { resolveTrustedProxyHops } from "./trusted-proxy-hops.js";
18
+ export async function runWithBootAbortCleanups(boot) {
19
+ const abortCleanups = [];
20
+ try {
21
+ return await boot((cleanup) => abortCleanups.push(cleanup));
22
+ }
23
+ catch (error) {
24
+ // Pools and clients opened before the failing step would otherwise keep the process alive.
25
+ for (const cleanup of abortCleanups.reverse()) {
26
+ await Promise.resolve()
27
+ .then(cleanup)
28
+ .catch(() => undefined);
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ let warnedLegacyJwtSecret = false;
34
+ function warnLegacyJwtSecretOnce() {
35
+ // skip: already warned once this process — avoid log spam on every boot path.
36
+ if (warnedLegacyJwtSecret)
37
+ return;
38
+ warnedLegacyJwtSecret = true;
39
+ createFallbackLogger("runProdApp").warn("JWT keyring carries a legacy (pre-rotation) verify-only key — " +
40
+ "it never expires on its own. JWT_SECRET remains a required env (hmacSecret " +
41
+ "fallback + boot requireEnv). To stop verifying with the pre-rotation secret: " +
42
+ "set auth.mail.hmacSecret explicitly, then rotate JWT_SECRET to a fresh value " +
43
+ "that is no longer present in any in-flight token (see resolveAuthMail / " +
44
+ "loadJwtSecretOrKeyring).");
45
+ }
46
+ export async function resolveProdBootConfig(options, decryptedEnvSource, log, registerAbortCleanup) {
47
+ // Stacks whose boot options are missing and the declarative boot options
48
+ // (mail, kmsMode, fileStorage, billing, demoMode, seed shortcuts) fail here, before any connection. Their defaults sit under
49
+ // the env so an explicitly set variable wins.
50
+ assertStackBootRequirements(options);
51
+ const bootOptionWiring = await resolveBootOptionWiring(options, decryptedEnvSource);
52
+ const envSource = withEnvDefaults(bootOptionWiring.envDefaults, decryptedEnvSource);
53
+ const bootKms = bootOptionWiring.kms;
54
+ registerAbortCleanup(() => bootKms.close());
55
+ // Polyfill before anything else — feature code references Temporal.
56
+ const { ensureTemporalPolyfill } = await import("@cosmicdrift/kumiko-framework/time");
57
+ await ensureTemporalPolyfill();
58
+ // Env-vars: fail-fast. Better a 0s boot crash with a clear error
59
+ // than a 30s timeout chasing a Postgres connection that was never
60
+ // configured.
61
+ const databaseUrl = requireEnv("DATABASE_URL", envSource);
62
+ const redisUrl = requireEnv("REDIS_URL", envSource);
63
+ // JWT_SECRET stays mandatory (also the resolveAuthMail hmacSecret
64
+ // fallback below — a separate, non-rotating HMAC token family, not the
65
+ // session JWT). jwtSecretOrKeyring is the OPTIONAL upgrade: set
66
+ // JWT_SECRET_V<n> + JWT_SECRET_CURRENT_VERSION for zero-downtime
67
+ // rotation of the session-JWT signing key — see loadJwtSecretOrKeyring.
68
+ const jwtSecret = requireEnv("JWT_SECRET", envSource);
69
+ const jwtSecretOrKeyring = loadJwtSecretOrKeyring(envSource);
70
+ if (typeof jwtSecretOrKeyring === "object" && Object.hasOwn(jwtSecretOrKeyring.keys, "legacy")) {
71
+ // Once per process — JWT_SECRET stays a required env (requireEnv above +
72
+ // resolveAuthMail hmacSecret fallback). Retiring the *legacy verify key*
73
+ // means setting auth.mail.hmacSecret explicitly and rotating JWT_SECRET
74
+ // to a fresh value that is no longer in any in-flight token; unsetting
75
+ // JWT_SECRET entirely hard-crashes boot.
76
+ warnLegacyJwtSecretOnce();
77
+ }
78
+ const jwtIssuer = readEnv("JWT_ISSUER", envSource);
79
+ const instanceId = readEnv("KUMIKO_INSTANCE_ID", envSource);
80
+ // options.auth.trustedProxyHops wins; falls back
81
+ // to the env var so ops can close the XFF-spoofing hole per-deployment
82
+ // without a code change (mirrors instanceId's env-first pattern above).
83
+ const trustedProxyHops = resolveTrustedProxyHops({
84
+ option: options.trustedProxyHops,
85
+ authOption: options.auth?.trustedProxyHops,
86
+ envValue: readEnv(TRUSTED_PROXY_HOPS_ENV, envSource),
87
+ nodeEnv: process.env["NODE_ENV"],
88
+ });
89
+ if (trustedProxyHops === undefined) {
90
+ // With 0 hops every request appears to come from the proxy pod, so all IP-keyed
91
+ // rate limiters collapse into one shared bucket behind a reverse proxy.
92
+ log.warn(`trustedProxyHops / ${TRUSTED_PROXY_HOPS_ENV} is not set; client IP falls back to the socket address. ` +
93
+ "Behind a reverse proxy all clients share one rate-limit bucket, so the default L1/L2 limits throttle the whole site at once.");
94
+ }
95
+ const resolvedRateLimit = resolveRateLimit(options.rateLimit);
96
+ const port = options.port ?? Number.parseInt(envSource["PORT"] ?? "3000", 10);
97
+ log.info(`booting Kumiko stack on port ${port}`);
98
+ return {
99
+ bootOptionWiring,
100
+ envSource,
101
+ bootKms,
102
+ databaseUrl,
103
+ redisUrl,
104
+ jwtSecret,
105
+ jwtSecretOrKeyring,
106
+ jwtIssuer,
107
+ instanceId,
108
+ trustedProxyHops,
109
+ resolvedRateLimit,
110
+ port,
111
+ };
112
+ }
113
+ export function composeProdFeatures(options, config, log) {
114
+ const { bootOptionWiring, envSource, bootKms, jwtSecret } = config;
115
+ // auth.mail is expanded into the explicit flow fields before feeding both
116
+ // the feature side (buildComposeAuthOptions) and the routes fragment below.
117
+ // From here on, always read effectiveAuth instead of options.auth.
118
+ const effectiveAuth = options.auth
119
+ ? resolveAuthMail(options.auth, jwtSecret, envSource, {
120
+ mailConfigured: bootOptionWiring.mailConfigured,
121
+ })
122
+ : undefined;
123
+ const authPublicOrigin = effectiveAuth
124
+ ? resolveAuthPublicOrigin(effectiveAuth, envSource, (message) => {
125
+ log.warn(message);
126
+ })
127
+ : undefined;
128
+ const { unsubscribeLinks, unsubscribeRoutes } = resolveUnsubscribeWiring({
129
+ unsubscribe: options.unsubscribe,
130
+ publicOrigin: effectiveAuth?.publicOrigin,
131
+ mailBaseUrl: effectiveAuth?.mail?.baseUrl,
132
+ envSource,
133
+ warn: (message) => createFallbackLogger("runProdApp").warn(message),
134
+ });
135
+ // Feature registry. Auth mode automatically mixes config/user/tenant/
136
+ // auth-email-password via composeFeatures, using the same feature list as
137
+ // runDevApp and the per-app schema generator. Build it before any connection
138
+ // so boot mode can validate wiring without opening Postgres or Redis sockets.
139
+ const features = composeAppFeatures(options.features, bootOptionWiring.features, effectiveAuth, options.bundledLists);
140
+ const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
141
+ validateComposedFeatures(features, {
142
+ env: envSource,
143
+ ...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
144
+ ...options.validateBootOptions,
145
+ });
146
+ assertPiiBootInvariants(features, {
147
+ kms: bootKms.kms,
148
+ blindIndexKey: bootKms.blindIndexKey,
149
+ allowPlaintextPii: bootKms.allowPlaintextPii,
150
+ mode: "prod",
151
+ });
152
+ const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
153
+ const registry = createRegistry(features);
154
+ assertSessionBootInvariants({
155
+ hasAuth: Boolean(effectiveAuth),
156
+ sessionStoreProviderMounted: registry.getExtensionUsages(EXT_SESSION_STORE).length > 0,
157
+ mode: "prod",
158
+ });
159
+ return {
160
+ effectiveAuth,
161
+ authPublicOrigin,
162
+ unsubscribeLinks,
163
+ unsubscribeRoutes,
164
+ features,
165
+ bootCrypto,
166
+ sessionsFeature,
167
+ registry,
168
+ };
169
+ }
@@ -0,0 +1,17 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
3
+ import { createRedisCacheSyncBus } from "@cosmicdrift/kumiko-framework/redis";
4
+ import { Redis } from "ioredis";
5
+ import { type DatabaseWaitClock } from "./boot/wait-for-database.ts";
6
+ import type { RunProdAppOptions } from "./run-prod-app.ts";
7
+ import type { ProdBootConfig, RegisterBootAbortCleanup } from "./run-prod-app-config.ts";
8
+ export type ProdConnections = {
9
+ readonly db: DbConnection;
10
+ readonly closeDb: () => Promise<void>;
11
+ readonly outsideTransactionDb: DbConnection;
12
+ readonly closeOutsideTransactionDb: () => Promise<void>;
13
+ readonly redis: Redis;
14
+ readonly cacheSync: ReturnType<typeof createRedisCacheSyncBus>;
15
+ };
16
+ export declare function connectProdDependencies(config: ProdBootConfig, log: Logger, registerAbortCleanup: RegisterBootAbortCleanup, databaseWait?: DatabaseWaitClock): Promise<ProdConnections>;
17
+ export declare function assertProdSchemaCurrent(db: DbConnection, migrations: RunProdAppOptions["migrations"], log: Logger): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { assertKumikoSchemaCurrent, SchemaDriftError, } from "@cosmicdrift/kumiko-framework/migrations";
2
+ import { createRedisCacheSyncBus, logRedisConnectionState, redisClientOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
3
+ import { Redis } from "ioredis";
4
+ import { openDatabasePools, waitForKmsHealthy } from "./boot/shared-boot-steps.js";
5
+ import { WAIT_FOR_DATABASE_DEADLINE_MS, waitForDatabase, } from "./boot/wait-for-database.js";
6
+ // Postgres + Redis. The Redis client is shared by idempotency, event-dedup,
7
+ // entity-cache, rate-limit; failing to construct here surfaces the misconfig
8
+ // immediately. `new Redis(...)` connects eagerly, so this must stay AFTER the
9
+ // boot-mode exit.
10
+ export async function connectProdDependencies(config, log, registerAbortCleanup, databaseWait) {
11
+ const { bootKms, databaseUrl, redisUrl, envSource } = config;
12
+ // KMS health gate: an app configured for crypto-shredding must not boot
13
+ // against an unreachable key store — a silent start would answer every
14
+ // PII read with 503. Runs BEFORE connections so an abort leaks nothing.
15
+ const databaseWaitDeadline = (databaseWait?.now ?? Date.now)() + WAIT_FOR_DATABASE_DEADLINE_MS;
16
+ if (bootKms.kms) {
17
+ await waitForKmsHealthy(bootKms.kms, "runProdApp", databaseWaitDeadline, databaseWait);
18
+ }
19
+ const pools = openDatabasePools(databaseUrl, envSource);
20
+ const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
21
+ registerAbortCleanup(pools.closeDb);
22
+ registerAbortCleanup(pools.closeOutsideTransactionDb);
23
+ registerAbortCleanup(() => redis.disconnect());
24
+ logRedisConnectionState(redis, log);
25
+ await waitForDatabase(pools.db, {
26
+ ...databaseWait,
27
+ processName: "runProdApp",
28
+ deadline: databaseWaitDeadline,
29
+ });
30
+ // One bus per process: the tier resolver, the dispatcher and the file-provider
31
+ // resolver all subscribe to it (invalidation across the API pods).
32
+ const cacheSync = createRedisCacheSyncBus({
33
+ redisUrl,
34
+ clientOptions: redisClientOptionsFromEnv(envSource),
35
+ });
36
+ return { ...pools, redis, cacheSync };
37
+ }
38
+ // Schema-drift gate (without drizzle, using kumiko/migrations). `kumiko
39
+ // schema apply` runs as a deploy step before container rollout. Boot only
40
+ // verifies that every kumiko/migrations/*.sql file is applied in
41
+ // _kumiko_migrations with an unchanged checksum and that every table in
42
+ // kumiko/migrations/.snapshot.json exists. Drift aborts boot rather than being auto-healed, avoiding
43
+ // races between container replicas. `migrations: false` opts out for custom
44
+ // schema setups.
45
+ export async function assertProdSchemaCurrent(db, migrations, log) {
46
+ if (migrations !== false) {
47
+ const migrationsDir = migrations?.dir ?? "./kumiko/migrations";
48
+ log.info(`checking schema drift (${migrationsDir})`);
49
+ try {
50
+ await assertKumikoSchemaCurrent(db, migrationsDir);
51
+ }
52
+ catch (err) {
53
+ if (err instanceof SchemaDriftError) {
54
+ log.error(`BOOT ABORTED — ${err.message}`);
55
+ }
56
+ throw err;
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,14 @@
1
+ import type { EffectiveFeaturesResolver } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type AllInOneEntrypoint, type ApiEntrypoint, type ApiEntrypointOptions } from "@cosmicdrift/kumiko-framework/entrypoint";
3
+ import type { RunProdAppOptions } from "./run-prod-app.ts";
4
+ import { type ProdAuthWiring } from "./run-prod-app-auth.ts";
5
+ import type { ProdBootConfig, ProdComposedFeatures } from "./run-prod-app-config.ts";
6
+ import type { ProdConnections } from "./run-prod-app-connections.ts";
7
+ import type { ProdRequestContext } from "./run-prod-app-request-context.ts";
8
+ export type ProdEntrypoint = ApiEntrypoint | AllInOneEntrypoint;
9
+ export declare function buildProdEntrypointOptions(options: RunProdAppOptions, config: ProdBootConfig, composed: ProdComposedFeatures, connections: ProdConnections, requestContext: ProdRequestContext, authWiring: ProdAuthWiring, effectiveFeatures: EffectiveFeaturesResolver | undefined): ApiEntrypointOptions;
10
+ export type ProdEntrypointOptions = ReturnType<typeof buildProdEntrypointOptions>;
11
+ export declare function createProdEntrypoint(options: RunProdAppOptions, config: ProdBootConfig, composed: ProdComposedFeatures, connections: ProdConnections, entrypointOptions: ProdEntrypointOptions): ProdEntrypoint;
12
+ export declare function registerProdShutdownHooks(entrypoint: ProdEntrypoint, cacheSync: ProdConnections["cacheSync"], ownedRedisSseBroker: ProdRequestContext["ownedRedisSseBroker"]): void;
13
+ export declare function runProdWireHook(options: RunProdAppOptions, composed: ProdComposedFeatures, connections: ProdConnections, entrypoint: ProdEntrypoint): Promise<void>;
14
+ export declare function startProdBackgroundWork(entrypoint: ProdEntrypoint, config: ProdBootConfig, composed: ProdComposedFeatures, connections: ProdConnections): Promise<void>;