@cosmicdrift/kumiko-server-runtime 0.386.0 → 0.387.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.
@@ -1,42 +1,42 @@
1
1
  /// <reference types="bun-types" preserve="true" />
2
- // runProdApp — production-grade Bootstrap-Wrapper für Kumiko-Apps.
2
+ // runProdApp — production-grade bootstrap wrapper for Kumiko apps.
3
3
  //
4
- // Symmetrisch zu runDevApp, aber:
5
- // - DATABASE_URL / REDIS_URL / JWT_SECRET aus env (fail-fast bei Boot,
6
- // keine ephemeralen Test-DBs)
7
- // - Hard Schema-Drift-Gate: prüft kumiko/migrations vs. _kumiko_migrations
8
- // + tableExists für jede erwartete Tabelle. KEIN Auto-CREATE TABLE im
9
- // Boot — Migration ist ein CI-Step (`bun kumiko schema apply`), Boot
10
- // validiert nur. Verhindert Race-Conditions bei Multi-Replica-Deploys
11
- // + macht Schema-Stand reviewbar in der Pull-Request.
12
- // - Idempotente Seeds: laufen nur wenn DB leer (über `isDbEmpty`-Probe
13
- // pro Seed). Re-Boots nach erstem Seed sind no-op.
14
- // - HTTP-Server via Bun.serve mit graceful SIGTERM/SIGINT → drain().
15
- // - Auth-Routes + bundled-features auto-mix wenn `auth:` gesetzt
16
- // (gleiche Logik wie runDevApp).
4
+ // Symmetric with runDevApp, but:
5
+ // - DATABASE_URL / REDIS_URL / JWT_SECRET come from env (fail-fast at boot;
6
+ // no ephemeral test databases)
7
+ // - Hard schema-drift gate: checks kumiko/migrations against
8
+ // _kumiko_migrations and verifies every expected table with tableExists.
9
+ // Boot never runs CREATE TABLE automatically; migrations are applied by
10
+ // CI (`bun kumiko schema apply`) and boot only validates them. This avoids
11
+ // races in multi-replica deployments and keeps schema state reviewable.
12
+ // - Idempotent seeds run only when the database is empty, using an
13
+ // `isDbEmpty` probe per seed. Reboots after the first seed are no-ops.
14
+ // - HTTP server via Bun.serve with graceful SIGTERM/SIGINT → drain().
15
+ // - Auth routes and bundled features are mixed automatically when `auth:` is
16
+ // set, using the same logic as runDevApp.
17
17
  //
18
- // App-Author schreibt:
18
+ // App authors write:
19
19
  // await runProdApp({ features, envSchema, auth, anonymousAccess, seeds });
20
20
  //
21
- // Container/Coolify setzt:
21
+ // Container/Coolify sets:
22
22
  // DATABASE_URL=postgresql://...
23
23
  // REDIS_URL=redis://...
24
- // JWT_SECRET=<random-32+> (always required — also signs the password-
25
- // reset/email-verification HMAC tokens, a separate non-rotating family)
26
- // — additionally, for zero-downtime rotation of the session-JWT
27
- // signing key specifically:
24
+ // JWT_SECRET=<random-32+> (always required — also signs password-reset and
25
+ // email-verification HMAC tokens, a separate non-rotating token family)
26
+ // — additionally, for zero-downtime rotation of the session-JWT signing
27
+ // key specifically:
28
28
  // JWT_SECRET_V1=<random-32+> (repeat _V2, _V3, ... per rotation)
29
29
  // JWT_SECRET_CURRENT_VERSION=1 (which V<n> signs new tokens; the others
30
30
  // still verify in-flight tokens until they expire)
31
- // Adopting rotation for the first time (no JWT_SECRET_V<n> set yet): the
32
- // plain JWT_SECRET above stays set and is carried into the keyring as a
33
- // verify-only legacy key (loadJwtSecretOrKeyring), so sessions signed
34
- // before the cutover keep verifying — no mass-logout at adoption. This
35
- // legacy key never expires on its own (boot logs a warning while it's
36
- // present) — once max token TTL has elapsed since cutover, unset
37
- // JWT_SECRET to retire it. Check first whether an app relies on the
38
- // auth.mail convenience default for passwordReset/emailVerification
39
- // hmacSecret (falls back to this same JWT_SECRET) before unsetting it.
31
+ // When adopting rotation for the first time (with no JWT_SECRET_V<n> set),
32
+ // the plain JWT_SECRET remains set and is carried into the keyring as a
33
+ // verify-only legacy key (loadJwtSecretOrKeyring), so sessions signed before
34
+ // cutover continue to verify without a mass logout. This legacy key never
35
+ // expires automatically; boot logs a warning while it is present. Once the
36
+ // maximum token TTL has elapsed since cutover, unset JWT_SECRET to retire
37
+ // it. First check whether the app relies on auth.mail's convenience default
38
+ // for passwordReset/emailVerification hmacSecret, which falls back to this
39
+ // same JWT_SECRET.
40
40
  // PORT=3000
41
41
  // KUMIKO_INSTANCE_ID=<stable per replica>
42
42
  import { AuthErrors, AuthHandlers, AuthQueries, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
@@ -219,7 +219,7 @@ export async function runProdApp(options) {
219
219
  }
220
220
  const jwtIssuer = readEnv("JWT_ISSUER", envSource);
221
221
  const instanceId = readEnv("KUMIKO_INSTANCE_ID", envSource);
222
- // kumiko-framework#1539 — options.auth.trustedProxyHops wins; falls back
222
+ // options.auth.trustedProxyHops wins; falls back
223
223
  // to the env var so ops can close the XFF-spoofing hole per-deployment
224
224
  // without a code change (mirrors instanceId's env-first pattern above).
225
225
  const trustedProxyHops = resolveTrustedProxyHops({
@@ -237,9 +237,9 @@ export async function runProdApp(options) {
237
237
  const resolvedRateLimit = resolveRateLimit(options.rateLimit);
238
238
  const port = options.port ?? Number.parseInt(envSource["PORT"] ?? "3000", 10);
239
239
  log.info(`booting Kumiko stack on port ${port}`);
240
- // auth.mail → expandiert in die expliziten Flow-Felder, bevor sie sowohl
241
- // die Feature-Side (buildComposeAuthOptions) als auch das Routes-Fragment
242
- // unten speisen. Ab hier IMMER effectiveAuth statt options.auth lesen.
240
+ // auth.mail is expanded into the explicit flow fields before feeding both
241
+ // the feature side (buildComposeAuthOptions) and the routes fragment below.
242
+ // From here on, always read effectiveAuth instead of options.auth.
243
243
  const effectiveAuth = options.auth
244
244
  ? resolveAuthMail(options.auth, jwtSecret, envSource, {
245
245
  mailConfigured: bootOptionWiring.mailConfigured,
@@ -257,11 +257,10 @@ export async function runProdApp(options) {
257
257
  envSource,
258
258
  warn: (message) => createFallbackLogger("runProdApp").warn(message),
259
259
  });
260
- // 3. Feature registry. Auth-mode auto-mixes config/user/tenant/auth-email-
261
- // password via composeFeatures — same source-of-truth as runDevApp
262
- // AND the per-app drizzle-Schema-Generator, so Migration und Runtime
263
- // sehen exakt dieselbe Liste. Built BEFORE any connection so boot-mode
264
- // can validate wiring and exit without opening a Postgres/Redis socket.
260
+ // 3. Feature registry. Auth mode automatically mixes config/user/tenant/
261
+ // auth-email-password via composeFeatures, using the same feature list as
262
+ // runDevApp and the per-app schema generator. Build it before any connection
263
+ // so boot mode can validate wiring without opening Postgres or Redis sockets.
265
264
  const composeAuthOptions = buildComposeAuthOptions(effectiveAuth);
266
265
  const features = composeFeatures([...options.features, ...bootOptionWiring.features], {
267
266
  includeBundled: !!effectiveAuth,
@@ -330,7 +329,7 @@ export async function runProdApp(options) {
330
329
  const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
331
330
  logRedisConnectionState(redis, log);
332
331
  await waitForDatabase(db, { processName: "runProdApp" });
333
- // Sprint-8a Tier-Composition auto-wire: scan features for a
332
+ // Tier-Composition auto-wire: scan features for a
334
333
  // tenantTierResolver-extension. If found, build the resolver here (db +
335
334
  // registry are available) before the dispatcher is constructed. App-Author sees
336
335
  // nothing — `createTierEngineFeature(opts)` mounts + framework auto-wires.
@@ -358,14 +357,13 @@ export async function runProdApp(options) {
358
357
  });
359
358
  resolvedEffectiveFeatures = bound.resolver;
360
359
  }
361
- // 5. Schema-Drift-Gate (drizzle-frei, kumiko/migrations). `kumiko schema
362
- // apply` läuft als Deploy-Step VOR dem Container-Rollout. Boot prüft nur:
363
- // (a) Alle Migrations aus kumiko/migrations/*.sql sind in
364
- // _kumiko_migrations applied (+ checksum unverändert)
365
- // (b) Alle Tabellen aus kumiko/migrations/.snapshot.json existieren
366
- // Drift = Boot-Error mit klarer Meldung (kein Auto-Heal — mehrere
367
- // Container-Replicas würden sonst race-conditionen fahren). Opt-out via
368
- // `migrations: false` für custom Schema-Setups.
360
+ // 5. Schema-drift gate (without drizzle, using kumiko/migrations). `kumiko
361
+ // schema apply` runs as a deploy step before container rollout. Boot only
362
+ // verifies that every kumiko/migrations/*.sql file is applied in
363
+ // _kumiko_migrations with an unchanged checksum and that every table in
364
+ // kumiko/migrations/.snapshot.json exists. Drift aborts boot rather than being auto-healed, avoiding
365
+ // races between container replicas. `migrations: false` opts out for custom
366
+ // schema setups.
369
367
  if (options.migrations !== false) {
370
368
  const migrationsDir = options.migrations?.dir ?? "./kumiko/migrations";
371
369
  log.info(`checking schema drift (${migrationsDir})`);
@@ -383,27 +381,20 @@ export async function runProdApp(options) {
383
381
  const idempotency = createIdempotencyGuard(redis, { ttlSeconds: 60 });
384
382
  const eventDedup = createEventDedup(redis, { ttlSeconds: 60 });
385
383
  const entityCache = createEntityCache(redis, { ttlSeconds: 60 });
386
- // 7. Lifecycle is built by createApiEntrypoint when not supplied —
387
- // we let the entrypoint own it and read it back through the handle
388
- // for SIGTERM.
384
+ // 7. When no lifecycle is supplied, createApiEntrypoint builds and owns it;
385
+ // the handle exposes it for SIGTERM.
389
386
  //
390
- // extraContext + anonymousAccess sind factory-union: entweder direktes
391
- // Object oder Function die {db, redis, registry} bekommt und das Object
392
- // returned. Factory-Form gilt als bevorzugt für Cases die zur Boot-Zeit
393
- // gegen die DB resolven müssen (z.B. Subdomain-Tenant-Lookup im
394
- // tenantResolver) — die Factory closure'd `db` und der Resolver kann
395
- // sie zur Request-Zeit aufrufen.
396
- // Build the sseBroker here (instead of letting createApiEntrypoint do it
397
- // internally) so extraContext factories can close over it at boot time —
398
- // e.g. an extraContext provider that publishes SSE events directly. We
399
- // pass the same broker into createApiEntrypoint (sseBroker? option) so
400
- // the server-internal broadcast and app-specific publishes run over
401
- // exactly one broker. createDefaultSseBroker (not createSseBroker
402
- // directly) is what makes that broker Redis-backed under REDIS_URL —
403
- // an unconditional in-memory broker here would silently win over
404
- // buildServer's own REDIS_URL default (ServerOptions.sseBroker always
405
- // wins), which is exactly how fw#2630's cross-replica fanout fix shipped
406
- // without ever taking effect in production.
387
+ // extraContext and anonymousAccess accept either an object or a factory that
388
+ // receives {db, redis, registry}; factories are useful when boot-time database
389
+ // access is needed, such as a subdomain-to-tenant resolver.
390
+ //
391
+ // Build the SSE broker here so extraContext factories can close over it, and
392
+ // pass the same broker into createApiEntrypoint (sseBroker option) so
393
+ // server-internal broadcasts and app-specific publishes share one broker. Use
394
+ // createDefaultSseBroker so REDIS_URL selects a Redis-backed broker; an
395
+ // unconditional in-memory broker would silently override buildServer's
396
+ // Redis-backed default (ServerOptions.sseBroker always wins) and prevent
397
+ // cross-replica fanout.
407
398
  const { sseBroker, ownedRedisSseBroker } = createDefaultSseBroker(envSource);
408
399
  const deps = {
409
400
  db,
@@ -417,20 +408,19 @@ export async function runProdApp(options) {
417
408
  const resolvedExtraContext = typeof options.extraContext === "function"
418
409
  ? options.extraContext(deps)
419
410
  : (options.extraContext ?? {});
420
- // Framework-Default-Provider zuerst, App-Werte (resolvedExtraContext)
421
- // gewinnen immer (z.B. money-horse's eigener configResolver).
422
- // App-wide cipher for `encrypted: true` entity fields — executors resolve
423
- // it lazily, entities without encrypted fields never touch it.
411
+ // Install framework defaults first; app values in resolvedExtraContext always
412
+ // win (for example, an app-specific configResolver).
413
+ // The app-wide cipher is resolved lazily, so entities without encrypted
414
+ // fields never access it.
424
415
  configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
425
- // Subject-key KMS for pii-annotated fields (crypto-shredding, #724).
416
+ // Subject-key KMS for pii-annotated fields (crypto-shredding).
426
417
  // No adapter only gets this far when assertPiiBootInvariants above let it
427
418
  // through: no PII carriers, or an explicit allowPlaintextPii opt-out. The
428
419
  // engine then stays off and those fields are stored in plaintext.
429
420
  configurePiiSubjectKms(bootKms.kms);
430
- // Blind-Index-Key (#818) — Boot-Gate: mit aktivem KMS würden lookupable-
431
- // Felder als Ciphertext gespeichert, ohne Key gäbe es keinen bidx-Arm und
432
- // jeder Equality-Lookup (Login by email!) liefe ins Leere. Fail-fast statt
433
- // silent-broken-auth.
421
+ // Blind-index key boot gate: with KMS enabled, lookupable fields are stored as
422
+ // ciphertext. Without the key, equality lookups such as login by email cannot
423
+ // work, so fail fast rather than allowing silently broken authentication.
434
424
  configureBlindIndexKey(bootKms.blindIndexKey);
435
425
  const bootLogger = loggerFromExtraContext(resolvedExtraContext);
436
426
  if (bootLogger)
@@ -456,21 +446,19 @@ export async function runProdApp(options) {
456
446
  const baseAnonymousAccess = typeof options.anonymousAccess === "function"
457
447
  ? options.anonymousAccess(deps)
458
448
  : options.anonymousAccess;
459
- // #1374: tenantResolver / tenantExists come from auth-foundation providers.
449
+ // tenantResolver / tenantExists come from auth-foundation providers.
460
450
  const resolvedAnonymousAccess = await resolveAnonymousAccessFromRegistry(baseAnonymousAccess, {
461
451
  db,
462
452
  registry,
463
453
  warn: (message) => createFallbackLogger("runProdApp").warn(message),
464
454
  });
465
- // Sessions opt-in: db ist hier schon konkret (createDbConnection oben),
466
- // also direkt verdrahten — kein late-bound nötig wie bei runDevApp.
467
- // Ein JWT ohne sid wird abgelehnt, sobald ein sessionChecker verdrahtet ist —
468
- // Prod-Sessions können nicht stillschweigend umgangen werden. sessionMassRevoker
469
- // (4. callback aus createSessionCallbacks) ist nicht Teil der
470
- // AuthRoutesConfig-Surface — der geht via bindAutoRevokeFromFeature ans
471
- // sessions-Feature (Password-Change/-Reset revoked alle Sessions), nicht
472
- // über die auth-routes.
473
- // Secure-by-default (#1372): sessionStore provider → resolveSessionStore.
455
+ // Sessions are wired directly because db has already been created; unlike
456
+ // runDevApp, no late binding is needed. Once a sessionChecker is wired, JWTs
457
+ // without sid are rejected, so production sessions cannot be bypassed.
458
+ // sessionMassRevoker (the fourth callback from createSessionCallbacks) is not
459
+ // part of AuthRoutesConfig; bindAutoRevokeFromFeature hands it to the sessions
460
+ // feature so password change and reset revoke all sessions. Secure by default: the sessionStore provider
461
+ // goes through resolveSessionStore.
474
462
  const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
475
463
  if (mfaFeature)
476
464
  bindMfaTokenSecretsFromFeature(mfaFeature)?.(jwtSecret, envSource);
@@ -551,7 +539,7 @@ export async function runProdApp(options) {
551
539
  // Redis-backed, not the in-memory default createAuthRoutes falls
552
540
  // back to — an in-process limiter only rate-limits within a single
553
541
  // replica, so a multi-instance prod deployment would silently give
554
- // each replica its own bucket (#1262/#1274). Redis is required infra
542
+ // each replica its own bucket. Redis is required infra
555
543
  // here already (REDIS_URL), so this is free.
556
544
  loginRateLimit: createRedisLoginRateLimiter(redis),
557
545
  ...(effectiveAuth.cookieDomain !== undefined && {
@@ -621,17 +609,15 @@ export async function runProdApp(options) {
621
609
  }),
622
610
  ...(resolvedAnonymousAccess && { anonymousAccess: resolvedAnonymousAccess }),
623
611
  };
624
- // Deploy-Topologie. Default (Single-Container): createAllInOneEntrypoint
625
- // fährt HTTP + BEIDE Job-Lanes (zwei Runner, jeder schedult seine eigene
626
- // Lane-Crons → kein Double-Fire) + Event-Dispatcher inline. So laufen
627
- // worker-Lane-Crons (run-export-jobs default runIn:"worker") UND MSPs ohne
628
- // separaten Worker-Process — die Asymmetrie, an der der Daten-Export hing.
629
- // runSingleInstance:false → API-only; ein dezidierter Worker MUSS dann die
630
- // worker-Lane + MSPs übernehmen (api-Lane-Jobs laufen weiter lokal).
631
- // Default single-instance. eventDispatcher.disabled ist die Alt-Art, MSPs
632
- // diesem Prozess wegzunehmen (dezidierter Worker) — als runSingleInstance:
633
- // false honorieren (api-only, kein lokaler Dispatcher), damit der explizite
634
- // Flag Vorrang behält aber Bestands-Caller nicht brechen.
612
+ // Deployment topology. By default, createAllInOneEntrypoint runs HTTP, both
613
+ // job lanes (two runners, each scheduling only its own lane's crons, so
614
+ // nothing fires twice) and the event dispatcher inline. That way worker-lane
615
+ // crons (run-export-jobs defaults to runIn:"worker") and MSPs run without a
616
+ // separate worker process, the gap the data export used to stall on.
617
+ // runSingleInstance:false means API-only: a dedicated worker must then take
618
+ // over the worker lane and MSPs, while API-lane jobs keep running locally.
619
+ // The legacy eventDispatcher.disabled maps to runSingleInstance:false so
620
+ // existing callers keep working; the explicit flag takes precedence.
635
621
  const runSingleInstance = options.runSingleInstance ?? options.eventDispatcher?.disabled !== true;
636
622
  const hasJobs = registry.getAllJobs().size > 0;
637
623
  const queueNamePrefix = options.jobs?.queueNamePrefix;
@@ -670,7 +656,7 @@ export async function runProdApp(options) {
670
656
  // entrypoint's. Registered after createApiEntrypoint/createAllInOneEntrypoint
671
657
  // (which already registered the eventDispatcher/jobRunner hooks above),
672
658
  // so it drains LIFO before those, matching the close-before-teardown
673
- // order fw#2630 established for buildServer's own hook.
659
+ // order established for buildServer's own hook.
674
660
  entrypoint.lifecycle.registerShutdownHook("prodAppCacheSync", async () => {
675
661
  await cacheSync.close();
676
662
  });
@@ -684,7 +670,7 @@ export async function runProdApp(options) {
684
670
  // createApiEntrypoint/createAllInOneEntrypoint, before this function ever
685
671
  // runs. `wire` is the remaining escape-hatch for app-wired co-running
686
672
  // components that need the system-write dispatcher but don't declare a
687
- // route (kumiko-framework#3050).
673
+ // route.
688
674
  if (options.wire) {
689
675
  await options.wire({
690
676
  db,
@@ -746,18 +732,17 @@ export async function runProdApp(options) {
746
732
  });
747
733
  }
748
734
  }
749
- // ES-Operations / Seed-Migrations (Phase 1). Läuft NACH applyBootSeeds +
750
- // existing seeds-array — die deklarativen Seeds sind die "always-insert-
751
- // if-missing"-Schicht; seed-migrations sind die "diff-and-update"-
752
- // Schicht für Drift den existing Seeds nicht erfassen können (z.B.
753
- // Membership-Roles-Change nach initialer Seed-Erstellung).
735
+ // ES operations and seed migrations run after applyBootSeeds and the existing
736
+ // seeds array. Declarative seeds insert missing records; seed migrations
737
+ // diff and update state that existing seeds cannot revise, such as membership
738
+ // roles changed after initial seed creation.
754
739
  if (seedDispatcher && options.seedsDir !== undefined && envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
755
740
  await createEsOperationsTable(db);
756
741
  await runPendingSeedMigrations({
757
742
  db,
758
743
  seedsDir: options.seedsDir,
759
744
  appliedBy: "boot",
760
- registry, // → dry-run-validator catched handler-QN-typos vor dem write
745
+ registry, // The dry-run validator catches handler QN typos before writing.
761
746
  // @wrapper-known semantic-alias
762
747
  createContext: (dbRunner) => createSeedMigrationContext({ dispatcher: seedDispatcher, dbRunner }),
763
748
  });
@@ -800,15 +785,13 @@ export async function runProdApp(options) {
800
785
  fetch: fetchHandler,
801
786
  webSocketUpgradeFetch,
802
787
  listen: async (listenPort = port) => {
803
- // Bun.serve is the production HTTP. Tests don't call listen()
804
- // because vitest runs under Node where Bun.serve doesn't exist.
805
- // Options-Shape (inkl. idleTimeout: 0 für SSE) liegt in der
806
- // exportierten buildBunServeOptions-Funktion — siehe ihren
807
- // Header für die Begründung.
788
+ // Bun.serve provides production HTTP. Tests run under Node, so they call the
789
+ // fetch handler directly instead. buildBunServeOptions centralizes the
790
+ // options shape, including `idleTimeout: 0` for SSE.
808
791
  if (typeof Bun === "undefined") {
809
- // Klare Fehlermeldung statt nackter ReferenceError. Trifft wenn
810
- // jemand listen() unter Node/vitest aufruft ohne autoListen:false
811
- // — hilft beim Debug, statt sich an "Bun is not defined" abzumühen.
792
+ // Throw a clear error instead of a raw ReferenceError when listen() is called
793
+ // under Node/vitest without autoListen:false, making the runtime
794
+ // requirement explicit.
812
795
  throw new Error("[runProdApp] listen() requires Bun runtime (Bun.serve). " +
813
796
  "Under Node/vitest pass `autoListen: false` and call the returned `fetch()` directly.");
814
797
  }
@@ -3,11 +3,11 @@
3
3
  // Hono app, no auth routes, no SSE broker, no seeds. Shares the boot core
4
4
  // with runProdApp (env fail-fast, Temporal polyfill, composeFeatures/
5
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.
6
+ // gate, boot-crypto, extraContext). Before this function, every app
7
+ // deploying a worker rebuilt this boot by hand, and every deviation was
8
+ // silent: without `ensureTemporalPolyfill`, every job in the worker fails
9
+ // with "Temporal is not defined" in a retry loop, with no boot-time signal
10
+ // that anything is wrong.
11
11
  //
12
12
  // App-author writes:
13
13
  // await runWorkerApp({ features, wireComponents: async (deps) => {...} });
@@ -5,7 +5,7 @@ export type SessionBootGateOptions = {
5
5
  * silently skip server-side revocation and any session-list UI stays
6
6
  * empty); dev only warns — a forgotten sessions mount in a local sample
7
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). */
8
+ * either. */
9
9
  readonly mode: "prod" | "dev";
10
10
  };
11
11
  export declare function assertSessionBootInvariants(opts: SessionBootGateOptions): void;
@@ -1,5 +1,5 @@
1
1
  // Catch a forgotten sessions mount at boot instead of silently degrading
2
- // into stateless JWTs (#1372). Mount createSessionsFeature() for revocable
2
+ // into stateless JWTs. Mount createSessionsFeature() for revocable
3
3
  // sessions; there is no auth.sessions opt-out anymore.
4
4
  export function assertSessionBootInvariants(opts) {
5
5
  // skip: no auth mounted — nothing to gate.
@@ -2,7 +2,7 @@
2
2
  * runProdApp session-wiring decision (extracted pure so it is testable without a
3
3
  * full prod boot).
4
4
  *
5
- * Secure-by-default (#1372): mounting a sessionStore provider (sessions feature)
5
+ * Secure-by-default: mounting a sessionStore provider (sessions feature)
6
6
  * turns server-side session revocation ON automatically. There is no
7
7
  * `auth.sessions` opt-in/opt-out — mount sessions for revocable JWTs, omit it
8
8
  * for intentional stateless JWTs (boot-gate warns / aborts unless acknowledged
@@ -2,7 +2,7 @@
2
2
  * runProdApp session-wiring decision (extracted pure so it is testable without a
3
3
  * full prod boot).
4
4
  *
5
- * Secure-by-default (#1372): mounting a sessionStore provider (sessions feature)
5
+ * Secure-by-default: mounting a sessionStore provider (sessions feature)
6
6
  * turns server-side session revocation ON automatically. There is no
7
7
  * `auth.sessions` opt-in/opt-out — mount sessions for revocable JWTs, omit it
8
8
  * for intentional stateless JWTs (boot-gate warns / aborts unless acknowledged
@@ -14,7 +14,7 @@
14
14
  //
15
15
  // The status code alone used to be the only signal ("404 = no match") —
16
16
  // that masked any matched route's deliberate 404 (e.g. default-deny reads)
17
- // as the SPA shell with status 200 (kumiko-framework#2435). buildServer
17
+ // as the SPA shell with status 200. buildServer
18
18
  // (framework/api/server.ts) registers app.notFound() LAST on the app object
19
19
  // and marks exactly the "no handler found" case with
20
20
  // NO_ROUTE_MATCH_HEADER_NAME — a handler that builds its own
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-server-runtime",
3
- "version": "0.386.0",
3
+ "version": "0.387.0",
4
4
  "description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold tooling.",
5
- "license": "BUSL-1.1",
5
+ "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
7
  "repository": {
8
8
  "type": "git",
@@ -88,18 +88,18 @@
88
88
  }
89
89
  },
90
90
  "dependencies": {
91
- "@cosmicdrift/kumiko-bundled-features": "0.386.0",
92
- "@cosmicdrift/kumiko-framework": "0.386.0",
93
- "@cosmicdrift/kumiko-renderer": "0.386.0",
94
- "@cosmicdrift/kumiko-renderer-mail-html": "0.386.0",
95
- "@cosmicdrift/kumiko-types": "0.386.0",
91
+ "@cosmicdrift/kumiko-bundled-features": "0.387.0",
92
+ "@cosmicdrift/kumiko-framework": "0.387.0",
93
+ "@cosmicdrift/kumiko-renderer": "0.387.0",
94
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.387.0",
95
+ "@cosmicdrift/kumiko-types": "0.387.0",
96
96
  "bun-types": "^1.3.13",
97
97
  "ioredis": "^5.10.1",
98
98
  "temporal-polyfill": "^0.3.2",
99
99
  "zod": "^4.4.3"
100
100
  },
101
101
  "devDependencies": {
102
- "@cosmicdrift/kumiko-testing": "0.386.0"
102
+ "@cosmicdrift/kumiko-testing": "0.387.0"
103
103
  },
104
104
  "publishConfig": {
105
105
  "registry": "https://registry.npmjs.org",