@cosmicdrift/kumiko-server-runtime 0.328.1 → 0.330.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 (90) hide show
  1. package/README.md +15 -0
  2. package/dist/boot/apply-boot-seeds.d.ts +8 -0
  3. package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
  4. package/dist/boot/boot-crypto.d.ts +9 -0
  5. package/dist/boot/boot-crypto.js +39 -0
  6. package/dist/boot/job-run-logger.d.ts +19 -0
  7. package/dist/boot/job-run-logger.js +44 -0
  8. package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
  9. package/dist/boot/pii-event-backfill-on-boot.js +25 -0
  10. package/dist/build-prod-bundle.d.ts +103 -0
  11. package/dist/build-prod-bundle.js +751 -0
  12. package/dist/bun-serve-options.d.ts +32 -0
  13. package/dist/bun-serve-options.js +49 -0
  14. package/dist/compose-features.d.ts +31 -0
  15. package/dist/compose-features.js +124 -0
  16. package/dist/extra-routes-deps.d.ts +30 -0
  17. package/dist/extra-routes-deps.js +4 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +7 -0
  20. package/dist/pii-boot-gate.d.ts +13 -0
  21. package/dist/pii-boot-gate.js +46 -0
  22. package/dist/renderer-web-fonts.d.ts +9 -0
  23. package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
  24. package/dist/resolve-tailwind-cli.d.ts +14 -0
  25. package/dist/resolve-tailwind-cli.js +36 -0
  26. package/dist/run-prod-app-boot-context.d.ts +43 -0
  27. package/dist/run-prod-app-boot-context.js +198 -0
  28. package/dist/run-prod-app-static-files.d.ts +32 -0
  29. package/dist/run-prod-app-static-files.js +280 -0
  30. package/dist/run-prod-app.d.ts +471 -0
  31. package/dist/run-prod-app.js +741 -0
  32. package/dist/run-worker-app.d.ts +92 -0
  33. package/dist/run-worker-app.js +220 -0
  34. package/dist/security-headers.d.ts +20 -0
  35. package/dist/security-headers.js +49 -0
  36. package/dist/session-boot-gate.d.ts +11 -0
  37. package/dist/session-boot-gate.js +22 -0
  38. package/dist/session-wiring.d.ts +11 -0
  39. package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
  40. package/dist/try-hono-first.d.ts +24 -0
  41. package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
  42. package/package.json +102 -36
  43. package/src/changes.json +6 -0
  44. package/src/__tests__/boot-extra-context.test.ts +0 -163
  45. package/src/__tests__/boot-probe-fixture.ts +0 -64
  46. package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
  47. package/src/__tests__/build-prod-bundle.test.ts +0 -724
  48. package/src/__tests__/cache-headers.test.ts +0 -83
  49. package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
  50. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -609
  51. package/src/__tests__/compose-features.test.ts +0 -206
  52. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  53. package/src/__tests__/pii-boot-gate.test.ts +0 -112
  54. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  55. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  56. package/src/__tests__/renderer-web-theme-plugin.integration.test.ts +0 -77
  57. package/src/__tests__/require-env.test.ts +0 -34
  58. package/src/__tests__/resolve-auth-mail.test.ts +0 -144
  59. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  60. package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
  61. package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
  62. package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
  63. package/src/__tests__/run-prod-app-spec.test.ts +0 -167
  64. package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
  65. package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
  66. package/src/__tests__/run-worker-app-boot.test.ts +0 -183
  67. package/src/__tests__/run-worker-app.integration.test.ts +0 -218
  68. package/src/__tests__/security-headers.test.ts +0 -88
  69. package/src/__tests__/session-boot-gate.test.ts +0 -70
  70. package/src/__tests__/session-wiring.test.ts +0 -16
  71. package/src/__tests__/try-hono-first.test.ts +0 -108
  72. package/src/__tests__/websocket-route.integration.test.ts +0 -580
  73. package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
  74. package/src/boot/__tests__/job-run-logger.test.ts +0 -26
  75. package/src/boot/boot-crypto.ts +0 -67
  76. package/src/boot/job-run-logger.ts +0 -63
  77. package/src/boot/pii-event-backfill-on-boot.ts +0 -43
  78. package/src/build-prod-bundle.ts +0 -996
  79. package/src/bun-serve-options.ts +0 -110
  80. package/src/compose-features.ts +0 -197
  81. package/src/extra-routes-deps.ts +0 -38
  82. package/src/index.ts +0 -27
  83. package/src/pii-boot-gate.ts +0 -80
  84. package/src/resolve-tailwind-cli.ts +0 -45
  85. package/src/run-prod-app-boot-context.ts +0 -319
  86. package/src/run-prod-app-static-files.ts +0 -359
  87. package/src/run-prod-app.ts +0 -1420
  88. package/src/run-worker-app.ts +0 -366
  89. package/src/security-headers.ts +0 -67
  90. package/src/session-boot-gate.ts +0 -34
@@ -1,366 +0,0 @@
1
- // runWorkerApp — production-grade bootstrap wrapper for a dedicated
2
- // Kumiko worker process. Symmetric to runProdApp, but without HTTP: no
3
- // Hono app, no auth routes, no SSE broker, no seeds. Shares the boot core
4
- // with runProdApp (env fail-fast, Temporal polyfill, composeFeatures/
5
- // registry, PII invariants, KMS health gate, connections, schema-drift
6
- // gate, boot-crypto, extraContext) — see fw#1725: before this function,
7
- // every app deploying a worker rebuilt this boot by hand (solon#42), and
8
- // every deviation was silent: without `ensureTemporalPolyfill`, every job
9
- // in the worker fails with "Temporal is not defined" in a retry loop,
10
- // with no boot-time signal that anything is wrong.
11
- //
12
- // App-author writes:
13
- // await runWorkerApp({ features, wireComponents: async (deps) => {...} });
14
- //
15
- // Container/Coolify sets the same env vars as runProdApp:
16
- // DATABASE_URL, REDIS_URL, JWT_SECRET — PORT is not needed.
17
-
18
- import { loadJwtSecretOrKeyring, type ServerOptions } from "@cosmicdrift/kumiko-framework/api";
19
- import {
20
- configureBlindIndexKey,
21
- configurePiiSubjectKms,
22
- type KmsAdapter,
23
- resolvePlatformKeks,
24
- } from "@cosmicdrift/kumiko-framework/crypto";
25
- import {
26
- configureEntityFieldEncryption,
27
- createDbConnection,
28
- type DbConnection,
29
- } from "@cosmicdrift/kumiko-framework/db";
30
- import {
31
- createRegistry,
32
- type EffectiveFeaturesResolver,
33
- type FeatureDefinition,
34
- findTierResolverUsage,
35
- type Registry,
36
- type TierResolverPlugin,
37
- type ValidateBootOptions,
38
- validateBoot,
39
- } from "@cosmicdrift/kumiko-framework/engine";
40
- import {
41
- createWorkerEntrypoint,
42
- type WorkerEntrypoint,
43
- } from "@cosmicdrift/kumiko-framework/entrypoint";
44
- import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
45
- import {
46
- assertKumikoSchemaCurrent,
47
- SchemaDriftError,
48
- } from "@cosmicdrift/kumiko-framework/migrations";
49
- import type {
50
- ObservabilityOptions,
51
- ObservabilityProvider,
52
- } from "@cosmicdrift/kumiko-framework/observability";
53
- import {
54
- createEntityCache,
55
- createEventDedup,
56
- createIdempotencyGuard,
57
- } from "@cosmicdrift/kumiko-framework/pipeline";
58
- import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
59
- import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
60
- import Redis from "ioredis";
61
- import { resolveBootCrypto } from "./boot/boot-crypto";
62
- import { jobRunLoggerCallbacks } from "./boot/job-run-logger";
63
- import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot";
64
- import { composeFeatures } from "./compose-features";
65
- import { makeDispatchSystemWrite, type SystemWireDeps } from "./extra-routes-deps";
66
- import { assertPiiBootInvariants } from "./pii-boot-gate";
67
- import { requireEnv } from "./run-prod-app";
68
- import { addConfigAccessorFactory, buildBootExtraContext } from "./run-prod-app-boot-context";
69
-
70
- export type WorkerContextOption =
71
- | Record<string, unknown>
72
- | ((deps: WorkerDeps) => Record<string, unknown>);
73
-
74
- export type WorkerDeps = {
75
- readonly db: DbConnection;
76
- readonly redis: Redis;
77
- readonly registry: Registry;
78
- };
79
-
80
- /** Deps for the `wireComponents` hook — app-wired co-running components
81
- * (an analysis runner, an IMAP supervisor, ...) that need the system-
82
- * write dispatcher and register their own shutdown hooks on the worker
83
- * lifecycle. Same shape as the `wire` hook's SystemWireDeps, plus
84
- * `lifecycle` for `registerShutdownHook`. */
85
- export type WorkerWireDeps = SystemWireDeps & {
86
- readonly lifecycle: WorkerEntrypoint["lifecycle"];
87
- };
88
-
89
- export type RunWorkerAppOptions = {
90
- /** App-specific features — same array as in the API/all-in-one process,
91
- * so the registry + schema stay identical across processes. */
92
- readonly features: readonly FeatureDefinition[];
93
- /** Opt-in boot-validator warnings — see ValidateBootOptions. */
94
- readonly validateBootOptions?: ValidateBootOptions;
95
- /** Mount the auto-mixed config/user/tenant/auth-email-password features —
96
- * MUST match the API process's `includeBundled` value, otherwise API
97
- * and worker run with a diverging registry topology. Also controls
98
- * whether buildBootExtraContext auto-wires `configResolver` (same
99
- * auth-mode gate as runProdApp). */
100
- readonly includeBundled?: boolean;
101
- /** Path to kumiko/migrations for the boot gate. See RunProdAppOptions
102
- * ["migrations"] — identical semantics. */
103
- readonly migrations?: { readonly dir: string } | false;
104
- /** Extra AppContext keys — same factory-union pattern as
105
- * RunProdAppOptions["extraContext"], without sseBroker (the worker
106
- * has none — see entrypoint/index.ts's documented SSE limitation). */
107
- readonly extraContext?: WorkerContextOption;
108
- /** MasterKeyProvider for ctx.secrets. Default: env-KEK (see
109
- * RunProdAppOptions["masterKey"]). */
110
- readonly masterKey?: MasterKeyProvider;
111
- /** Subject-key adapter for crypto-shredding — boot checks health()
112
- * before any connection (see RunProdAppOptions["kms"]). */
113
- readonly kms?: KmsAdapter;
114
- /** Blind-index key for lookupable fields (see
115
- * RunProdAppOptions["blindIndexKey"]). */
116
- readonly blindIndexKey?: string;
117
- /** Explicit opt-out from the PII boot gate (see
118
- * RunProdAppOptions["allowPlaintextPii"]). */
119
- readonly allowPlaintextPii?: string;
120
- readonly jobs?: {
121
- readonly queueNamePrefix?: string;
122
- /** Source of active tenant ids for `perTenant: true` jobs (see
123
- * RunProdAppOptions["jobs"]["getActiveTenantIds"]) — identical
124
- * semantics, framework falls back to the tenant feature's own
125
- * active-tenant-ids query when omitted. */
126
- readonly getActiveTenantIds?: JobRunnerOptions["getActiveTenantIds"];
127
- };
128
- /** Tuning knobs for the event-dispatcher loop (pollIntervalMs, pgClient
129
- * for LISTEN/NOTIFY). */
130
- readonly eventDispatcher?: ServerOptions["eventDispatcher"];
131
- /** Hook for app-wired co-running components that need the system-write
132
- * dispatcher (e.g. an analysis runner, an IMAP supervisor). Runs AFTER
133
- * `entrypoint.start()` — the hook itself is responsible for starting
134
- * its component and registering a shutdown hook on `lifecycle`. */
135
- readonly wireComponents?: (deps: WorkerWireDeps) => Promise<void> | void;
136
- /** Feature-toggle resolver (see RunProdAppOptions["effectiveFeatures"]). */
137
- readonly effectiveFeatures?: EffectiveFeaturesResolver;
138
- /** Override `process.env` for env-validation (see
139
- * RunProdAppOptions["envSource"]). */
140
- readonly envSource?: Record<string, string | undefined>;
141
- /** Env names to decrypt from their `_CIPHERTEXT` twin at boot, typically
142
- * `kmsSlotsOf(composedEnv.schema)`. The worker parses no env schema, so it
143
- * cannot derive them; without this it reads the env as is. */
144
- readonly kmsSlots?: readonly string[];
145
- readonly observability?: ObservabilityProvider;
146
- readonly observabilityOptions?: ObservabilityOptions;
147
- };
148
-
149
- export type WorkerAppHandle = {
150
- /** In KUMIKO_DRY_RUN_ENV=boot mode with an injected envSource (test
151
- * path), no boot ran — this slot is an undefined-cast, do not access. */
152
- readonly entrypoint: WorkerEntrypoint;
153
- readonly stop: () => Promise<void>;
154
- };
155
-
156
- function makeBootModeHandle(): WorkerAppHandle {
157
- return {
158
- // @cast-boundary boot-mode: no entrypoint exists because no boot ran —
159
- // callers on this path never read this slot.
160
- entrypoint: undefined as unknown as WorkerEntrypoint,
161
- stop: async () => {},
162
- };
163
- }
164
-
165
- export async function runWorkerApp(options: RunWorkerAppOptions): Promise<WorkerAppHandle> {
166
- const rawEnvSource = options.envSource ?? process.env;
167
- const envSource: Record<string, string | undefined> = options.kmsSlots
168
- ? await resolvePlatformKeks(rawEnvSource, {
169
- slots: options.kmsSlots,
170
- logPrefix: "[runWorkerApp]",
171
- })
172
- : rawEnvSource;
173
-
174
- // 1. Polyfill before anything else — exactly the bug fw#1725 reports:
175
- // without it, every job in the worker fails with "Temporal is not
176
- // defined" in a retry loop, with no boot-time signal.
177
- const { ensureTemporalPolyfill } = await import("@cosmicdrift/kumiko-framework/time");
178
- await ensureTemporalPolyfill();
179
-
180
- // 2. Env vars: fail-fast, same requireEnv as runProdApp. JWT_SECRET is
181
- // validated by loadJwtSecretOrKeyring itself (throws when neither
182
- // JWT_SECRET nor JWT_SECRET_V<n> is set).
183
- const databaseUrl = requireEnv("DATABASE_URL", envSource, "runWorkerApp");
184
- const redisUrl = requireEnv("REDIS_URL", envSource, "runWorkerApp");
185
- const jwtSecretOrKeyring = loadJwtSecretOrKeyring(envSource);
186
-
187
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
188
- console.log("[runWorkerApp] booting Kumiko worker…");
189
-
190
- // 3. Feature registry — identical to runProdApp, MUST be built with the
191
- // same `includeBundled` as the API process.
192
- const features = composeFeatures(options.features, {
193
- includeBundled: !!options.includeBundled,
194
- });
195
- validateBoot(features, { env: envSource, ...options.validateBootOptions });
196
- warnIfNonUtcServerTimeZone();
197
- assertPiiBootInvariants(features, {
198
- kms: options.kms,
199
- blindIndexKey: options.blindIndexKey,
200
- allowPlaintextPii: options.allowPlaintextPii,
201
- mode: "prod",
202
- });
203
- const registry = createRegistry(features);
204
-
205
- // Boot-mode exit (parity with runProdApp's C1): validators ran and the
206
- // registry is built, no DB/Redis client was constructed. Makes the boot
207
- // testable without real Postgres/Redis infra.
208
- if (envSource["KUMIKO_DRY_RUN_ENV"] === "boot") {
209
- // biome-ignore lint/suspicious/noConsole: boot-mode output IS the deliverable
210
- console.log(
211
- `[runWorkerApp] boot validation OK (${features.length} features, ${registry.features.size} registry entries)`,
212
- );
213
- if (options.envSource === undefined) {
214
- process.exit(0);
215
- }
216
- return makeBootModeHandle();
217
- }
218
-
219
- // 4. KMS health gate — runs BEFORE the connections, so an abort leaks
220
- // nothing (identical to runProdApp).
221
- if (options.kms) {
222
- const kmsHealth = await options.kms.health();
223
- if (!kmsHealth.ok) {
224
- throw new Error(
225
- `[runWorkerApp] BOOT ABORTED — KMS health check failed (latency ${kmsHealth.latencyMs}ms)`,
226
- );
227
- }
228
- }
229
-
230
- const { db, close: closeDb } = createDbConnection(databaseUrl);
231
- const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
232
-
233
- let resolvedEffectiveFeatures: EffectiveFeaturesResolver | undefined = options.effectiveFeatures;
234
- if (resolvedEffectiveFeatures === undefined) {
235
- const tierResolverUsage = findTierResolverUsage(features);
236
- if (tierResolverUsage) {
237
- const plugin = tierResolverUsage.options as TierResolverPlugin;
238
- resolvedEffectiveFeatures = await plugin.build({ db, registry });
239
- }
240
- }
241
-
242
- // 5. Schema-drift gate — identical to runProdApp.
243
- if (options.migrations !== false) {
244
- const migrationsDir = options.migrations?.dir ?? "./kumiko/migrations";
245
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint
246
- console.log(`[runWorkerApp] checking schema drift (${migrationsDir})…`);
247
- try {
248
- await assertKumikoSchemaCurrent(db, migrationsDir);
249
- } catch (err) {
250
- if (err instanceof SchemaDriftError) {
251
- // biome-ignore lint/suspicious/noConsole: terminal error message
252
- console.error(`\n[runWorkerApp] BOOT ABORTED — ${err.message}\n`);
253
- }
254
- throw err;
255
- }
256
- }
257
-
258
- // 6. Pipeline pieces — same defaults as runProdApp.
259
- const idempotency = createIdempotencyGuard(redis, { ttlSeconds: 60 });
260
- const eventDedup = createEventDedup(redis, { ttlSeconds: 60 });
261
- const entityCache = createEntityCache(redis, { ttlSeconds: 60 });
262
-
263
- // 7. Boot-crypto + extraContext — the core of fw#1725: KMS wiring,
264
- // blind-index, config-encryption must be wired in the worker exactly
265
- // like in the API, otherwise the worker writes rows the API can no
266
- // longer read (different cipher, missing blind index).
267
- const deps: WorkerDeps = { db, redis, registry };
268
- const resolvedExtraContext =
269
- typeof options.extraContext === "function"
270
- ? options.extraContext(deps)
271
- : (options.extraContext ?? {});
272
-
273
- const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
274
- configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
275
- configurePiiSubjectKms(options.kms);
276
- configureBlindIndexKey(options.blindIndexKey);
277
- const autoExtraContext = buildBootExtraContext({
278
- db,
279
- features,
280
- envSource,
281
- registry,
282
- hasAuth: !!options.includeBundled,
283
- crypto: bootCrypto,
284
- ...(options.kms && { kms: options.kms }),
285
- });
286
- const extraContext = addConfigAccessorFactory(
287
- { ...autoExtraContext, ...resolvedExtraContext },
288
- registry,
289
- );
290
-
291
- const jobLogger = jobRunLoggerCallbacks(registry, db);
292
- const entrypoint = createWorkerEntrypoint({
293
- registry,
294
- context: { db, redis, entityCache, registry, ...extraContext },
295
- jwtSecret: jwtSecretOrKeyring,
296
- dispatcherOptions: {
297
- idempotency,
298
- ...(resolvedEffectiveFeatures && { effectiveFeatures: resolvedEffectiveFeatures }),
299
- },
300
- eventDedup,
301
- ...(options.observability && { observability: options.observability }),
302
- ...(options.observabilityOptions && { observabilityOptions: options.observabilityOptions }),
303
- redisUrl,
304
- ...jobLogger,
305
- ...(options.jobs?.queueNamePrefix !== undefined && {
306
- queueNamePrefix: options.jobs.queueNamePrefix,
307
- }),
308
- ...(options.jobs?.getActiveTenantIds !== undefined && {
309
- getActiveTenantIds: options.jobs.getActiveTenantIds,
310
- }),
311
- ...(options.eventDispatcher && { eventDispatcher: options.eventDispatcher }),
312
- });
313
-
314
- const handle: WorkerAppHandle = {
315
- entrypoint,
316
- stop: async () => {
317
- await entrypoint.stop();
318
- await closeDb();
319
- redis.disconnect();
320
- },
321
- };
322
-
323
- await entrypoint.start();
324
- startPiiEventBackfillOnBoot({
325
- db,
326
- registry,
327
- lifecycle: entrypoint.lifecycle,
328
- envSource,
329
- });
330
-
331
- if (options.wireComponents) {
332
- await options.wireComponents({
333
- db,
334
- redis,
335
- registry,
336
- dispatchSystemWrite: makeDispatchSystemWrite(entrypoint.dispatcher),
337
- lifecycle: entrypoint.lifecycle,
338
- });
339
- }
340
-
341
- let shuttingDown = false;
342
- const shutdown = async (signal: string) => {
343
- // skip: shutdown already in progress, avoid double-drain
344
- if (shuttingDown) return;
345
- shuttingDown = true;
346
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
347
- console.log(`[runWorkerApp] ${signal} received — draining…`);
348
- try {
349
- await handle.stop();
350
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
351
- console.log("[runWorkerApp] graceful shutdown complete.");
352
- } catch (e) {
353
- // biome-ignore lint/suspicious/noConsole: shutdown-time error, only path is stderr
354
- console.error("[runWorkerApp] error during shutdown:", e);
355
- } finally {
356
- process.exit(0);
357
- }
358
- };
359
- process.on("SIGTERM", () => void shutdown("SIGTERM"));
360
- process.on("SIGINT", () => void shutdown("SIGINT"));
361
-
362
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
363
- console.log("[runWorkerApp] ready — worker running.");
364
-
365
- return handle;
366
- }
@@ -1,67 +0,0 @@
1
- export type SecurityHeadersOption =
2
- | false
3
- | {
4
- /** `Strict-Transport-Security` value, or `false` to omit.
5
- * Default: `max-age=31536000; includeSubDomains`. */
6
- readonly hsts?: string | false;
7
- /** `X-Frame-Options` value, or `false` to omit. Default: `DENY`.
8
- * Apps that must be embeddable (iframe widgets) set `false` and
9
- * scope framing via a `csp` frame-ancestors directive instead. */
10
- readonly frameOptions?: string | false;
11
- /** `false` omits `X-Content-Type-Options: nosniff`. */
12
- readonly contentTypeOptions?: false;
13
- /** `Referrer-Policy` value, or `false` to omit.
14
- * Default: `strict-origin-when-cross-origin`. */
15
- readonly referrerPolicy?: string | false;
16
- /** `Content-Security-Policy` default for ALL responses. No built-in
17
- * default — a wrong CSP breaks app assets, so it stays opt-in.
18
- * A per-host CSP from `hostDispatch` wins over this value. */
19
- readonly csp?: string;
20
- };
21
-
22
- const DEFAULT_HSTS = "max-age=31536000; includeSubDomains";
23
- const DEFAULT_FRAME_OPTIONS = "DENY";
24
- const DEFAULT_REFERRER_POLICY = "strict-origin-when-cross-origin";
25
-
26
- export function resolveSecurityHeaders(
27
- option: SecurityHeadersOption | undefined,
28
- ): ReadonlyArray<readonly [string, string]> {
29
- if (option === false) return [];
30
- const opt = option ?? {};
31
- const headers: Array<readonly [string, string]> = [];
32
- const hsts = opt.hsts ?? DEFAULT_HSTS;
33
- if (hsts !== false) headers.push(["strict-transport-security", hsts]);
34
- const frameOptions = opt.frameOptions ?? DEFAULT_FRAME_OPTIONS;
35
- if (frameOptions !== false) headers.push(["x-frame-options", frameOptions]);
36
- if (opt.contentTypeOptions !== false) headers.push(["x-content-type-options", "nosniff"]);
37
- const referrerPolicy = opt.referrerPolicy ?? DEFAULT_REFERRER_POLICY;
38
- if (referrerPolicy !== false) headers.push(["referrer-policy", referrerPolicy]);
39
- if (opt.csp) headers.push(["content-security-policy", opt.csp]);
40
- return headers;
41
- }
42
-
43
- // Sets each header only when absent so per-response values (e.g. the
44
- // per-host CSP from hostDispatch) always win over the runtime default.
45
- export function withSecurityHeaders<TServeArg = string>(
46
- handler: (req: Request, serveArg?: TServeArg) => Response | Promise<Response>,
47
- option: SecurityHeadersOption | undefined,
48
- ): (req: Request, serveArg?: TServeArg) => Response | Promise<Response> {
49
- const defaults = resolveSecurityHeaders(option);
50
- if (defaults.length === 0) return handler;
51
- return async (req: Request, serveArg?: TServeArg): Promise<Response> => {
52
- const res = await handler(req, serveArg);
53
- try {
54
- for (const [name, value] of defaults) {
55
- if (!res.headers.has(name)) res.headers.set(name, value);
56
- }
57
- return res;
58
- } catch {
59
- // Immutable-headers Response (e.g. proxied fetch) — re-wrap.
60
- const headers = new Headers(res.headers);
61
- for (const [name, value] of defaults) {
62
- if (!headers.has(name)) headers.set(name, value);
63
- }
64
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
65
- }
66
- };
67
- }
@@ -1,34 +0,0 @@
1
- export type SessionBootGateOptions = {
2
- readonly hasAuth: boolean;
3
- readonly sessionStoreProviderMounted: boolean;
4
- /** prod fails hard on a missing sessionStore provider (stateless JWTs would
5
- * silently skip server-side revocation and any session-list UI stays
6
- * empty); dev only warns — a forgotten sessions mount in a local sample
7
- * app shouldn't crash the dev-server boot, but it must not stay silent
8
- * either (#2027 — dev previously had no gate here at all). */
9
- readonly mode: "prod" | "dev";
10
- };
11
-
12
- // Catch a forgotten sessions mount at boot instead of silently degrading
13
- // into stateless JWTs (#1372). Mount createSessionsFeature() for revocable
14
- // sessions; there is no auth.sessions opt-out anymore.
15
- export function assertSessionBootInvariants(opts: SessionBootGateOptions): void {
16
- // skip: no auth mounted — nothing to gate.
17
- if (!opts.hasAuth) return;
18
- // skip: sessionStore provider is wired (sessions feature).
19
- if (opts.sessionStoreProviderMounted) return;
20
-
21
- const tag = opts.mode === "prod" ? "runProdApp" : "runDevApp";
22
- const message =
23
- "auth is mounted but no sessionStore provider is registered. JWTs would be stateless " +
24
- "(no server-side revocation) and any session-list screen stays empty. Mount " +
25
- "createSessionsFeature() (@cosmicdrift/kumiko-bundled-features/sessions) alongside auth-foundation.";
26
-
27
- if (opts.mode === "dev") {
28
- // biome-ignore lint/suspicious/noConsole: boot-time ops warning, no logger configured this early
29
- console.warn(`[${tag}] ${message}`);
30
- // skip: dev mode, warning already logged above — never abort dev boot.
31
- return;
32
- }
33
- throw new Error(`[${tag}] BOOT ABORTED — ${message}`);
34
- }