@cosmicdrift/kumiko-dev-server 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.
@@ -19,16 +19,16 @@ export type RunDevAppAuthOptions = {
19
19
  /** Optional override of the login error → HTTP status map. Default
20
20
  * maps invalidCredentials → 401, noMembership → 403. */
21
21
  readonly loginErrorStatusMap?: Readonly<Record<string, number>>;
22
- /** Auth-Mail-Convenience — symmetrisch zu RunProdAppAuthOptions.mail.
23
- * Verdrahtet alle 4 Mail-Flows aus env-SMTP + Standard-Templates;
24
- * hmacSecret = `JWT_SECRET`-env (Dev-Fallback wenn ungesetzt). Ohne
25
- * `SMTP_HOST`-env bleiben die Flows unverdrahtet (Null-Transport-Guard).
26
- * Explizite Flow-Setups gewinnen über den mail-Default. */
22
+ /** Auth-mail convenience, symmetric with RunProdAppAuthOptions.mail.
23
+ * Wires all four mail flows from env-based SMTP settings and the default
24
+ * templates; hmacSecret is the `JWT_SECRET` environment value, with a dev
25
+ * fallback when unset. Without `SMTP_HOST`, the flows remain unwired
26
+ * (the null-transport guard). Explicit flow setups override the mail default. */
27
27
  readonly mail?: AuthMailOptions;
28
- /** Password-reset flow. Wenn gesetzt werden /api/auth/request-password-
29
- * reset + /api/auth/reset-password als Public-Routes gemounted UND
30
- * der request/confirm-Handler im auth-email-password-Feature wird
31
- * registriert. Symmetrisch zu RunProdAppAuthOptions.passwordReset. */
28
+ /** Password-reset flow. When set, mounts /api/auth/request-password-
29
+ * reset and /api/auth/reset-password as public routes and registers the
30
+ * request/confirm handlers in the auth-email-password feature. Symmetric
31
+ * with RunProdAppAuthOptions.passwordReset. */
32
32
  readonly passwordReset?: PasswordResetSetup;
33
33
  /** Email-verification flow. Symmetric to passwordReset. */
34
34
  readonly emailVerification?: EmailVerificationSetup;
@@ -36,11 +36,11 @@ export type RunDevAppAuthOptions = {
36
36
  readonly signup?: SignupSetup;
37
37
  /** Tenant-invite flow (magic link). Symmetric. */
38
38
  readonly invite?: InviteSetup;
39
- /** Account-unlock flow (#1266). Symmetric to RunProdAppAuthOptions. */
39
+ /** Account-unlock flow. Symmetric to RunProdAppAuthOptions. */
40
40
  readonly accountUnlock?: AccountUnlockSetup;
41
41
  /** Brute-force guard on the login handler (maxFailedAttempts,
42
42
  * lockoutDurationMinutes). Mounts no routes. Symmetric to
43
- * RunProdAppAuthOptions.accountLockout (kumiko-framework#1627). */
43
+ * RunProdAppAuthOptions.accountLockout. */
44
44
  readonly accountLockout?: AccountLockoutSetup;
45
45
  /** Domain attribute for both auth cookies (see
46
46
  * AuthRoutesConfig.cookieDomain). Symmetric to RunProdAppAuthOptions. */
@@ -55,7 +55,7 @@ export type RunDevAppAuthOptions = {
55
55
  /** Opt out of the Origin guard. Symmetric to RunProdAppAuthOptions. */
56
56
  readonly unsafeSkipOriginCheck?: boolean;
57
57
  /** Number of trusted reverse-proxy hops for client-IP derivation (see
58
- * AuthRoutesConfig.trustedProxyHops, kumiko-framework#1539). Symmetric to
58
+ * AuthRoutesConfig.trustedProxyHops). Symmetric to
59
59
  * RunProdAppAuthOptions — dev usually runs unproxied, so this is normally
60
60
  * left unset (default 0). */
61
61
  readonly trustedProxyHops?: number;
@@ -72,9 +72,9 @@ export type RunDevAppSeedsConfig = Omit<ProdSeedsConfig, "run"> & {
72
72
  readonly run?: readonly SeedFn[];
73
73
  };
74
74
  export type RunDevAppOptions = {
75
- /** App-spezifische Features. Im auth-mode werden config/user/tenant/
76
- * auth-email-password automatisch dazu gemischt — KEIN doppeltes
77
- * manuelles Hinzufügen nötig. */
75
+ /** App-specific features. In auth mode, config/user/tenant/
76
+ * auth-email-password are mixed in automatically, so they must not be
77
+ * added manually as well. */
78
78
  readonly features: readonly FeatureDefinition[];
79
79
  /** Opt-in boot-validator warnings — see ValidateBootOptions. */
80
80
  readonly validateBootOptions?: ValidateBootOptions;
@@ -94,38 +94,39 @@ export type RunDevAppOptions = {
94
94
  * through `runDevApp` show the same resolved head prod does. See
95
95
  * `CreateKumikoServerOptions.resolvePageHead` for the full contract. */
96
96
  readonly resolvePageHead?: PageHeadResolver;
97
- /** CSS-Entry. Default: package-export `@cosmicdrift/kumiko-renderer/web/styles.css`
98
- * wenn ein client-Entry gesetzt ist. `false` deaktiviert die CSS-Pipeline. */
97
+ /** CSS entry. Defaults to the package export
98
+ * `@cosmicdrift/kumiko-renderer/web/styles.css` when a client entry is set.
99
+ * `false` disables the CSS pipeline. */
99
100
  readonly stylesheet?: string | false;
100
101
  /** Tailwind `--watch` process on/off, see `CreateKumikoServerOptions.stylesheetWatch`. */
101
102
  readonly stylesheetWatch?: boolean;
102
- /** Eigenes HTML-Template; sonst minimal-Default (#root + client.js).
103
- * Im Multi-Entry-Mode ist es das Fallback-Template, wenn ein einzelner
104
- * Entry kein eigenes htmlPath setzt. */
103
+ /** Custom HTML template; otherwise uses a minimal default (#root + client.js).
104
+ * In multi-entry mode, this is the fallback template when an entry does not
105
+ * define its own htmlPath. */
105
106
  readonly htmlPath?: string;
106
- /** Listen-Port. Default 4173 (oder $PORT). */
107
+ /** Listening port. Defaults to 4173, or `$PORT` when set. */
107
108
  readonly port?: number;
108
- /** Extra-Verzeichnisse für den File-Watcher (Trigger Hot-Reload). */
109
+ /** Additional directories for the file watcher to trigger hot reload. */
109
110
  readonly watchDirs?: readonly string[];
110
- /** SIGINT/SIGTERM-Handler installieren (Default true; in Tests auf
111
- * false damit repeated boots keine Listener akkumulieren). */
111
+ /** Installs SIGINT/SIGTERM handlers. Defaults to true; tests can disable
112
+ * them so repeated boots do not accumulate listeners. */
112
113
  readonly installSignalHandlers?: boolean;
113
114
  /** Auth mode: standard features mixed in, auth routes wired, seedAdminGuarded
114
115
  * runs in onAfterSetup. Without `auth` the server stays in auto-mint JWT mode. */
115
116
  readonly auth?: RunDevAppAuthOptions;
116
- /** Eigene Seed-Funktionen, laufen nach dem Admin (wenn auth) in
117
- * Array-Reihenfolge. */
117
+ /** App-defined seed functions, run after the admin seed when auth is active,
118
+ * in array order. */
118
119
  readonly seeds?: readonly SeedFn[] | RunDevAppSeedsConfig;
119
- /** Extra-AppContext-Keys. `templateResolver` (immer) + `secrets` (wenn das
120
- * secrets-Feature gemountet ist) + `configResolver` (auth-mode) werden
121
- * automatisch ergänzt — App-Werte gewinnen. Symmetrisch zu runProdApp. */
120
+ /** Additional AppContext keys. `templateResolver` is always added; `secrets`
121
+ * is added when the secrets feature is mounted; and `configResolver` is
122
+ * added in auth mode. App values take precedence. Symmetric with runProdApp. */
122
123
  /** Boot default locale (BCP-47) for ctx.locale when the request carries
123
124
  * no locale signal. Merged into AppContext before extraContext (app wins). */
124
125
  readonly defaultLocale?: string;
125
126
  readonly extraContext?: CreateKumikoServerOptions["extraContext"];
126
- /** MasterKeyProvider für die auto-verdrahtete `ctx.secrets`. Default:
127
- * `createEnvMasterKeyProvider`. Override für KMS-Backends. Nur relevant
128
- * wenn das secrets-Feature gemountet ist. Symmetrisch zu runProdApp. */
127
+ /** MasterKeyProvider for the automatically wired `ctx.secrets`. Defaults to
128
+ * `createEnvMasterKeyProvider`; override it for KMS backends. Only relevant
129
+ * when the secrets feature is mounted. Symmetric with runProdApp. */
129
130
  readonly masterKey?: MasterKeyProvider;
130
131
  /** Subject KMS for PII-annotated fields, the dev counterpart of runProdApp({ kms }).
131
132
  * Unset: an ephemeral DB gets an InMemoryKmsAdapter plus a throwaway blind-index key;
@@ -133,32 +134,33 @@ export type RunDevAppOptions = {
133
134
  * createPgKmsAdapter against that DB there (in-memory DEKs vanish on restart).
134
135
  * `false`: explicit plaintext, with the boot warning. */
135
136
  readonly kms?: KmsAdapter | false;
136
- /** 32-Byte-Key (base64) für Blind-Index-HMACs — Pflicht sobald `kms`
137
- * gesetzt ist und lookupable-Felder gemountet sind (sonst Boot-Abbruch,
138
- * symmetrisch zu runProdApp). */
137
+ /** 32-byte base64 key for blind-index HMACs. Required when `kms` is set and
138
+ * lookupable fields are mounted; otherwise boot aborts. Symmetric with
139
+ * runProdApp. */
139
140
  readonly blindIndexKey?: string;
140
- /** Env-Quelle für die ENV→config-app-override-Brücke (Keys mit `env:`
141
- * bekommen ihren env-Wert als app-override-Default — symmetrisch zu
142
- * runProdApp). Default `process.env`. Injizierbar als Test-Seam, damit
143
- * Tests eigene env-Werte reichen statt `process.env` global zu mutieren. */
141
+ /** Environment source for the ENV-to-config app-override bridge. Keys with
142
+ * `env:` receive their environment value as the app-override default,
143
+ * symmetric with runProdApp.
144
+ * Defaults to `process.env`; injectable as a test seam so tests can provide
145
+ * custom environment values without mutating the global `process.env`. */
144
146
  readonly envSource?: Record<string, string | undefined>;
145
- /** Anonymous-Access für Public-Endpoints — Requests ohne JWT laufen
146
- * als Pseudo-User mit Rolle `anonymous` durch, wenn der Handler die
147
- * Rolle in `access.roles` führt. */
147
+ /** Anonymous access for public endpoints. Requests without a JWT proceed as
148
+ * a pseudo-user with the `anonymous` role when the handler includes that
149
+ * role in `access.roles`. */
148
150
  readonly anonymousAccess?: CreateKumikoServerOptions["anonymousAccess"];
149
151
  /** File-Storage-Provider — aktiviert die Upload-Routes (/api/files) +
150
152
  * `ctx.files`. Demos: `{ storageProvider: createInMemoryFileProvider() }`. */
151
153
  readonly files?: CreateKumikoServerOptions["files"];
152
- /** App-eigene HTTP-Routes (z.B. /feed.xml, /sitemap.xml) — wird ans
153
- * Hono-app gehängt, läuft VOR dem static-asset-Pfad. Symmetrisch zur
154
- * gleichnamigen Option in runProdApp. */
154
+ /** App-defined HTTP routes, such as /feed.xml and /sitemap.xml. They are
155
+ * attached to the Hono app before the static-asset path. Symmetric with the
156
+ * same-named option in runProdApp. */
155
157
  readonly extraRoutes?: CreateKumikoServerOptions["extraRoutes"];
156
158
  /** Framework-managed unsubscribe links and routes, same contract as
157
159
  * `RunProdAppOptions.unsubscribe`; the link base URL comes from `auth.mail.baseUrl`
158
160
  * or `APP_BASE_URL` (dev has no `auth.publicOrigin`). */
159
161
  readonly unsubscribe?: UnsubscribeOption;
160
162
  /** Hook for app-wired co-running components — symmetric to
161
- * runProdApp.wire (kumiko-framework#3050). */
163
+ * runProdApp.wire. */
162
164
  readonly wire?: CreateKumikoServerOptions["wire"];
163
165
  /** Same as `RunProdAppOptions["mail"]`: mounts `channel-email` and wires the auth mail flows. */
164
166
  readonly mail?: MailBootOption;
@@ -1,16 +1,16 @@
1
1
  // High-level dev-server wrapper for sample apps and showcases. In auth mode it
2
2
  // mixes in the standard features (config/user/tenant/auth-email-password),
3
- // wires AuthRoutesConfig plus the login error map and calls seedAdminGuarded()
4
- // in onAfterSetup, cutting a sample bootstrap from ~50 lines to 5-10.
3
+ // wires AuthRoutesConfig plus the login error map, and calls seedAdminGuarded()
4
+ // in onAfterSetup, reducing a sample bootstrap from roughly 50 lines to 5–10.
5
5
  //
6
- // Auto-mix passiert NUR im auth-mode. Ohne `auth`-Block bleibt der
7
- // Server im Auto-Mint-JWT-Modus (Dev-Default) und mischt nichts dazu —
8
- // Showcases die nur eine Domain demonstrieren wollen ohne Login-Flow
9
- // haben so nicht plötzlich vier zusätzliche Features in der Registry.
6
+ // Auto-mixing occurs only in auth mode. Without an `auth` block, the server
7
+ // remains in auto-mint-JWT mode (the dev default) and adds none of these
8
+ // features. Showcases that demonstrate a single domain without a login flow
9
+ // therefore do not unexpectedly gain four additional registry features.
10
10
  //
11
- // Wer maximale Kontrolle braucht (z.B. abweichende Auth-Wiring,
12
- // alternativer Membership-Query, eigener LoginRateLimiter): geht direkt
13
- // auf `createKumikoServer` aus @cosmicdrift/kumiko-dev-server.
11
+ // For maximum control, such as custom auth wiring, a different membership
12
+ // query, or a custom LoginRateLimiter, use `createKumikoServer` directly from
13
+ // @cosmicdrift/kumiko-dev-server.
14
14
  import { AuthErrors, AuthHandlers, AuthQueries, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
15
15
  import { seedAdminGuarded, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
16
16
  import { EXT_SESSION_STORE, resolveSessionStore, resolveTokenVerifier, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
@@ -37,10 +37,9 @@ import { captureEphemeralDevPiiKms, resolveDevPiiKms, } from "./resolve-dev-pii-
37
37
  import { renderWelcomeBanner } from "./welcome-banner.js";
38
38
  import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, requireEnv, resolveAuthMail, resolveUnsubscribeWiring, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
39
39
  export async function runDevApp(options) {
40
- // Auto-mix Standard-Features im auth-mode via composeFeatures (single
41
- // source of truth — auch runProdApp und der per-app drizzle-Schema-
42
- // Generator nutzen denselben Helper, damit Migration und Runtime nie
43
- // auseinanderdriften können).
40
+ // Automatically mixes standard features in auth mode through composeFeatures.
41
+ // runProdApp and the per-app Drizzle schema generator use the same helper,
42
+ // keeping migrations and runtime feature composition aligned.
44
43
  const rawEnvSource = options.envSource ?? process.env;
45
44
  const seedsConfig = options.seeds !== undefined && !isSeedFnList(options.seeds) ? options.seeds : undefined;
46
45
  const stackSeeds = options.seeds === undefined
@@ -80,9 +79,10 @@ export async function runDevApp(options) {
80
79
  features: options.features,
81
80
  ...(options.featureToggles && { featureToggles: true }),
82
81
  }, ["featureToggles"]);
83
- // auth.mail → normalisiert in die expliziten Flow-Felder (symmetrisch zu
84
- // runProdApp). hmacSecret = JWT_SECRET-env (fail-fast, symmetrisch zu
85
- // runProdApp). Ab hier IMMER effectiveAuth statt options.auth.
82
+ // Normalizes auth.mail into the explicit flow fields, symmetrically with
83
+ // runProdApp. hmacSecret is read from the JWT_SECRET environment value and
84
+ // fails fast when unavailable. From this point on, use effectiveAuth rather
85
+ // than options.auth.
86
86
  const effectiveAuth = options.auth
87
87
  ? resolveAuthMail(options.auth, requireEnv("JWT_SECRET", envSource, "runDevApp"), envSource, {
88
88
  mailConfigured: wiring.mailConfigured,
@@ -120,13 +120,13 @@ export async function runDevApp(options) {
120
120
  : hasBootEnvDefaults
121
121
  ? { ...options.validateBootOptions, env: baseBootEnv }
122
122
  : options.validateBootOptions;
123
- // Boot-Validation als allererstes — vor dem Server. Dieselbe
124
- // Fehlerklasse (unqualifizierte nav-/handler-QNs, screen-access etc.),
125
- // die früher nur runProdApp fing und sonst erst den Prod-Pod im
126
- // CrashLoopBackOff sterben ließ (#359). Wirft synchron, bevor ein
127
- // Socket aufgeht.
128
- // Resolved before validateBoot: an app-supplied masterKey provider must satisfy the
129
- // encrypted-field gate here exactly as it does in runProdApp.
123
+ // Run boot validation before creating the server. This catches the same
124
+ // errors runProdApp catches (unqualified navigation or handler QNs,
125
+ // screen-access violations), which would otherwise only show up as a prod pod
126
+ // in CrashLoopBackOff. It throws
127
+ // synchronously before a socket is opened.
128
+ // Resolve this before validateBoot: an app-supplied masterKey provider must
129
+ // satisfy the encrypted-field gate exactly as it does in runProdApp.
130
130
  const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
131
131
  validateBoot(features, {
132
132
  ...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
@@ -134,7 +134,7 @@ export async function runDevApp(options) {
134
134
  });
135
135
  warnIfNonUtcServerTimeZone();
136
136
  validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
137
- // Sprint-8a Tier-Composition auto-wire: scan features for a
137
+ // Tier-Composition auto-wire: scan features for a
138
138
  // tenantTierResolver-extension and wire a late-bound wrapper here; it is
139
139
  // filled in onAfterSetup (where stack.db + stack.registry are available).
140
140
  // The global operator toggles layer over the same wrapper.
@@ -155,27 +155,27 @@ export async function runDevApp(options) {
155
155
  }
156
156
  return tierResolverHolder.resolver(tenantId);
157
157
  }, {
158
- // Late-bind: der echte trialGate hängt erst nach plugin.build() am
159
- // holder.resolver. Delegieren (nicht kopieren), weil dieses
160
- // Wrapper-Closure vor build() konstruiert wird. Kein Trial → false.
158
+ // The real trialGate lands on holder.resolver only after plugin.build(). Delegate rather than
159
+ // copy it because this wrapper closure is created before build().
160
+ // Return false when no trial gate is available.
161
161
  trialGate: (tenantId, featureName) => tierResolverHolder.resolver?.trialGate?.(tenantId, featureName) ??
162
162
  Promise.resolve(false),
163
163
  })
164
164
  : undefined;
165
- // configResolver-default fürs config-feature — im auth-mode immer
166
- // hinzufügen, im no-auth-mode dem Caller überlassen. Factory-form
167
- // wird gewrap't damit der spread auf das aufgerufene Result greift,
168
- // nicht auf die function selbst (no-op). Die ENV→config-Brücke (Keys mit
169
- // `env:` → app-override-Default) läuft symmetrisch zu runProdApp; die
170
- // throwaway-Registry hier extrahiert nur die config-Keys, weil der
171
- // configResolver vor dem Server-Boot konstruiert wird (stack.registry
172
- // gibt's erst onAfterSetup) — createKumikoServer baut intern seine eigene.
173
- // App-wide cipher for `encrypted: true` entity fields (symmetrisch zu
174
- // runProdApp) — executors resolve it lazily.
165
+ // Provide the configResolver default for the config feature in auth mode;
166
+ // in no-auth mode, leave that choice to the caller. Wrap the factory so the
167
+ // spread applies to its result; spreading the function itself is a no-op. The
168
+ // ENV-to-config bridge supplies app-override defaults from `env:` keys.
169
+ // The temporary registry extracts only config keys because the resolver is
170
+ // built before server boot, and stack.registry only exists from onAfterSetup;
171
+ // createKumikoServer builds its own registry internally.
172
+ // The app-wide cipher for `encrypted: true` entity fields is resolved lazily
173
+ // by executors, symmetrically with runProdApp.
175
174
  configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
176
- // Subject-KMS + Blind-Index (symmetrisch zu runProdApp). Dev warnt bei
177
- // Klartext-PII statt zu failen; kms+lookupable ohne blindIndexKey bricht
178
- // auch hier (Lookups wären in jedem Modus kaputt).
175
+ // Configure Subject-KMS and blind-index handling, symmetrically with
176
+ // runProdApp. Dev warns instead of failing for plaintext PII, but kms with
177
+ // lookupable fields still requires blindIndexKey because lookups would fail
178
+ // in every mode.
179
179
  const piiKms = options.kmsMode !== undefined
180
180
  ? {
181
181
  kms: wiring.kms.kms,
@@ -206,7 +206,7 @@ export async function runDevApp(options) {
206
206
  logInfo("[runDevApp] PII encrypted with in-memory KMS (ephemeral DB); pass kms: false to opt out");
207
207
  }
208
208
  // Throwaway registry, built once and reused below — mirrors runProdApp's
209
- // `createRegistry(features)` right after validateBoot (#2027). Symmetric
209
+ // `createRegistry(features)` right after validateBoot. Symmetric
210
210
  // sessionStoreProviderMounted check to runProdApp's (registry.getExtension-
211
211
  // Usages, not a raw features.some(...) scan) so dev and prod never ask the
212
212
  // same question two different ways again.
@@ -220,11 +220,12 @@ export async function runDevApp(options) {
220
220
  const cfgExtra = registry
221
221
  ? mergeConfigResolverDefault(options.extraContext, registry, envSource, bootCrypto.configCipher)
222
222
  : options.extraContext;
223
- // Auto-wire templateResolver (immer) + secrets (feature-gated), symmetrisch zu
224
- // runProdApp. Anders als prod existiert die db hier erst im Factory-deps
225
- // (createKumikoServer baut den Stack), darum als Factory: buildBootExtraContext
226
- // mit hasAuth:false (configResolver kommt schon aus cfgExtra), App-Werte
227
- // (cfgExtra) gewinnen über die Boot-Defaults.
223
+ // Automatically wire templateResolver and feature-gated secrets, symmetrically
224
+ // with runProdApp. Unlike prod, the database is available here only in the
225
+ // factory dependencies because createKumikoServer builds the stack. Therefore
226
+ // use a factory: buildBootExtraContext runs with hasAuth:false because
227
+ // configResolver already comes from cfgExtra, and cfgExtra values override
228
+ // the boot defaults.
228
229
  const extraContext = (deps) => {
229
230
  const base = typeof cfgExtra === "function" ? cfgExtra(deps) : (cfgExtra ?? {});
230
231
  const bootLogger = loggerFromExtraContext(base);
@@ -248,7 +249,7 @@ export async function runDevApp(options) {
248
249
  };
249
250
  // Sessions: late-bound holder — resolveSessionStore needs concrete db+
250
251
  // registry (only after setupTestStack). Wired when sessions feature is
251
- // mounted (#1372); no auth.sessions opt-in anymore.
252
+ // mounted; no auth.sessions opt-in anymore.
252
253
  let sessionStore;
253
254
  const requireSessionStore = () => {
254
255
  if (!sessionStore) {
@@ -289,10 +290,10 @@ export async function runDevApp(options) {
289
290
  })(),
290
291
  }
291
292
  : {};
292
- // Parity with runProdApp (#1372 review): gate on sessionStore provider, not
293
+ // Parity with runProdApp: gate on sessionStore provider, not
293
294
  // the sessions feature name — a custom provider must also wire auth
294
295
  // callbacks. sessionStoreProviderMounted computed above via the registry
295
- // (same check as runProdApp, #2027) — not recomputed here.
296
+ // (same check as runProdApp) — not recomputed here.
296
297
  const sessionAuthFragment = effectiveAuth && sessionStoreProviderMounted
297
298
  ? {
298
299
  sessionCreator: (user, meta) => requireSessionStore().creator(user, meta),
@@ -397,7 +398,7 @@ export async function runDevApp(options) {
397
398
  },
398
399
  }),
399
400
  onAfterSetup: async (stack) => {
400
- // Sprint-8a: build tier-resolver BEFORE any seeds so seeds can rely
401
+ // Build tier-resolver BEFORE any seeds so seeds can rely
401
402
  // on the resolver being live (e.g. seed that writes a SystemAdmin's
402
403
  // tier-assignment can immediately read tier-cuts).
403
404
  if (tierResolverUsage) {
@@ -417,7 +418,7 @@ export async function runDevApp(options) {
417
418
  tierResolverHolder.resolver = bound.resolver;
418
419
  }
419
420
  if (effectiveAuth && stack.registry.getExtensionUsages(EXT_SESSION_STORE).length > 0) {
420
- // Secure-by-default (#1372): resolve sessionStore provider; bind
421
+ // Secure-by-default: resolve sessionStore provider; bind
421
422
  // password-change mass-revoke + MFA revokeAllOthers.
422
423
  const store = await resolveSessionStore({
423
424
  db: stack.db,
@@ -512,10 +513,11 @@ export function mergeConfigResolverDefault(ctx, registry, envSource, cipher) {
512
513
  ...(cipher && { cipher }),
513
514
  }),
514
515
  };
515
- // ctx.config wird per-Request aus configAccessorFactory geminted (siehe
516
- // addConfigAccessorFactory) — sonst bleibt ctx.config undefined und Handler
517
- // die es lesen (createFileProviderForTenant) werfen. Aus dem EFFEKTIVEN
518
- // Resolver (Caller-Override gewinnt) gebaut, symmetrisch zu runProdApp.
516
+ // Build ctx.config per request through configAccessorFactory (see
517
+ // addConfigAccessorFactory); otherwise
518
+ // ctx.config remains undefined and handlers such as createFileProviderForTenant
519
+ // throw. Build it from the effective resolver so a caller override wins,
520
+ // symmetrically with runProdApp.
519
521
  if (ctx === undefined)
520
522
  return addConfigAccessorFactory(defaults, registry);
521
523
  if (typeof ctx === "function") {
@@ -25,10 +25,11 @@ export function scaffoldAppFeature(options) {
25
25
  throw new Error(`scaffoldAppFeature: ${featureDir} already exists — refusing to overwrite`);
26
26
  }
27
27
  mkdirSync(featureDir, { recursive: true });
28
- // Volle App-Feature-Konvention (Referenz: bundled-features/tenant):
29
- // feature.ts = nur Registrierung, schema/ für Entities, i18n.ts für
30
- // Server-Keys, web/ für die Client-Seite, __tests__/ mit Boot-Test.
31
- // handlers/ + lib/ entstehen beim ersten echten Handler/Logik-Modul.
28
+ // App-feature layout (see bundled-features/tenant):
29
+ // feature.ts only registers the feature; schema/ contains entities; i18n.ts
30
+ // contains server keys; web/ contains the client side; __tests__/ contains
31
+ // the boot test.
32
+ // handlers/ and lib/ are created when the first handler or logic module is added.
32
33
  mkdirSync(join(featureDir, "schema"), { recursive: true });
33
34
  mkdirSync(join(featureDir, "web"), { recursive: true });
34
35
  mkdirSync(join(featureDir, "__tests__"), { recursive: true });
@@ -192,12 +192,10 @@ const FOUNDATION_FEATURES = [
192
192
  callExpression: "createSessionsFeature()",
193
193
  },
194
194
  ];
195
- // composeFeatures({ includeBundled: true }) auto-mountet diese 4 Foundation-
196
- // Features. Sie hier nochmal in APP_FEATURES zu schreiben löste den dedupe-
197
- // warn-Spam im scaffolded `bun dev` aus (PR #599 hat den createRegistry-
198
- // Crash gefangen, der Spam blieb bis hier). Filter wirkt defensiv: auch wenn
199
- // jemand scaffoldApp() direkt mit allen Bundled-Feature-Entries aufruft
200
- // rutschen die 4 nicht in run-config.ts.
195
+ // composeFeatures({ includeBundled: true }) auto-mounts these four foundation features.
196
+ // Listing them again in APP_FEATURES caused duplicate-registration warnings in
197
+ // scaffolded `bun dev`; filtering also protects direct scaffoldApp() calls that
198
+ // pass all bundled-feature entries, keeping these four out of run-config.ts.
201
199
  const COMPOSE_AUTO_MOUNTED_NAMES = new Set(["config", "user", "tenant", "auth-email-password"]);
202
200
  const MAX_LINE_WIDTH = 100;
203
201
  // Mirrors Biome's import sort so generated files pass `biome check` untouched:
@@ -367,7 +365,7 @@ function renderMain(appName) {
367
365
  'displayName: "Admin",',
368
366
  ]);
369
367
  // Subject-key KMS is optional locally (plaintext + loud warning) but required
370
- // under NODE_ENV=production (fw#2317 / #2339).
368
+ // under NODE_ENV=production.
371
369
  return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
372
370
  // composeFeatures + validateBoot + createRegistry without DB/Redis-connect
373
371
  // (see @cosmicdrift/kumiko-server-runtime runProdApp).
@@ -1,8 +1,8 @@
1
1
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { type ComposeFeaturesOptions } from "@cosmicdrift/kumiko-server-runtime/compose-features";
3
3
  export type SchemaApplyOptions = ComposeFeaturesOptions & {
4
- /** App-Features (z.B. APP_FEATURES aus run-config) — composed mit den
5
- * bundled-Features für den Projection-Rebuild-Registry. */
4
+ /** App features (e.g. APP_FEATURES from run-config), composed with the
5
+ * bundled features for the projection rebuild registry. */
6
6
  readonly features: readonly FeatureDefinition[];
7
7
  /** Default INIT_CWD ?? process.cwd(); Migrations unter <appCwd>/kumiko/migrations. */
8
8
  readonly appCwd?: string;
@@ -1,10 +1,10 @@
1
- // Standalone `kumiko schema apply` für Production-Bundles. Apps bündeln ein
2
- // dünnes bin/kumiko.ts das nur runStandaloneSchemaCli mit den App-Features
3
- // aufruft — die ganze Orchestrierung (Infra-Bootstrap, Migrations,
4
- // Projection-Rebuild) lebt hier statt als ~100-Zeilen-Boilerplate pro App.
1
+ // Standalone `kumiko schema apply` for production bundles. Apps bundle a thin
2
+ // bin/kumiko.ts that only calls runStandaloneSchemaCli with the app features;
3
+ // all orchestration (infrastructure bootstrap, migrations, projection rebuild)
4
+ // lives here instead of being repeated as ~100 lines of boilerplate per app.
5
5
  //
6
- // Der Pulumi-migrate-initContainer ruft `bun /app/kumiko.js schema apply`;
7
- // kumiko-build entdeckt das App-bin via findRepoRoot() und bündelt es.
6
+ // The Pulumi-migrate-initContainer calls `bun /app/kumiko.js schema apply`;
7
+ // kumiko-build discovers the app bin via findRepoRoot() and bundles it.
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { createDbConnection, runMigrationsFromDir } from "@cosmicdrift/kumiko-framework/db";
@@ -27,11 +27,11 @@ export async function runSchemaApply(opts) {
27
27
  }
28
28
  const { db, close } = createDbConnection({ url: dbUrl });
29
29
  try {
30
- // Infra-Tabellen (event-store + pipeline-state) zuerst, alle
31
- // tableExists-gated idempotent: auf einer Bestands-DB no-op, auf einer
32
- // leeren Greenfield-DB legen sie kumiko_events/_consumers/_projections an
33
- // bevor die App-Migrations dagegen laufen. Ohne das bricht eine leere DB
34
- // (z.B. cashcolt auf frischem CNPG) an `relation "kumiko_events" does not exist`.
30
+ // Create infrastructure tables (event store + pipeline state) first; all are
31
+ // tableExists-gated and idempotent: on an existing DB this is a no-op, while
32
+ // on an empty Greenfield DB they create kumiko_events/_consumers/_projections
33
+ // before the app migrations run. Without this, an empty DB (e.g. cashcolt
34
+ // on a fresh CNPG) fails with `relation "kumiko_events" does not exist`.
35
35
  console.log("\n Lege Framework-Infra-Tabellen an (idempotent)…");
36
36
  await createEventsTable(db);
37
37
  await createEventConsumerStateTable(db);
@@ -52,7 +52,7 @@ export async function runSchemaApply(opts) {
52
52
  // Projection rebuild: persistent queue instead of "only this run's
53
53
  // result.applied" — otherwise a failed rebuild stays silently stuck
54
54
  // forever, since the migration is already tracked applied and gets
55
- // skipped on the next apply (#2464). queueRebuildsFromMarkers persists
55
+ // skipped on the next apply. queueRebuildsFromMarkers persists
56
56
  // the marker tables BEFORE the rebuild; runPendingRebuilds unconditionally
57
57
  // also picks up open entries from earlier, failed runs — not just the
58
58
  // ones this run freshly applied.
@@ -1,24 +1,25 @@
1
- // Drizzle-Schema-Barrel: Framework-Infrastruktur-Tables.
1
+ // Drizzle schema barrel: framework infrastructure tables.
2
2
  //
3
- // Re-exportiert die Framework-eigenen Tables (Event-Store + Pipeline-State)
4
- // die jede App in der DB haben muss. App-Author schreibt:
3
+ // Re-exports the framework-owned tables (event store + pipeline state)
4
+ // that every app must have in its database. The app author writes:
5
5
  //
6
6
  // // drizzle/schema.ts
7
7
  // export * from "@cosmicdrift/kumiko-dev-server/drizzle-tables-auth-mode";
8
- // export * from "./schema.generated"; // App-eigene + Bundle-Entity-Tables
8
+ // export * from "./schema.generated"; // App-owned + bundle entity tables
9
9
  //
10
- // drizzle-kit sucht im schema-Module nur nach pgTable-Instances und
11
- // ignoriert alle anderen exports — `export *` ist hier sicher.
10
+ // drizzle-kit only looks for pgTable instances in the schema module and
11
+ // ignores all other exports — `export *` is safe here.
12
12
  //
13
- // Bundle-Entity-Tables (configValuesTable, tenantTable, userTable,
14
- // userSessionTable, tenantMembershipsTable etc.) sind bewusst NICHT hier:
15
- // sie kommen über schema.generated.ts via buildEntityTable aus den
16
- // r.entity()-Definitionen, das ist seit der entity.indexes-API die
17
- // Single-Source-of-Truth. Doppelte Re-Exports würden zwei pgTable-
18
- // Instances mit identischem Index-Namen erzeugen — drizzle-kit warnt.
13
+ // Bundle entity tables (configValuesTable, tenantTable, userTable,
14
+ // userSessionTable, tenantMembershipsTable, etc.) are intentionally NOT here:
15
+ // they come through schema.generated.ts via buildEntityTable from the
16
+ // r.entity() definitions, which has been the single source of truth since the
17
+ // entity.indexes API. Duplicate re-exports would create two pgTable instances
18
+ // with identical index names, causing a drizzle-kit warning.
19
19
  //
20
- // (Datei-Name "auth-mode" ist historisch — heute ist der Inhalt reine
21
- // Framework-Infra. Umbenennen ist breaking change für App-Imports.)
22
- // Framework-Infra (immer da, unabhängig von Features)
20
+ // The file name "auth-mode" is historical — its contents are now purely
21
+ // framework infrastructure. Renaming it would be a breaking change for app
22
+ // imports.
23
+ // Framework infrastructure (always present, regardless of features)
23
24
  export { archivedStreamsTable, eventsTable, snapshotsTable, } from "@cosmicdrift/kumiko-framework/event-store";
24
25
  export { eventConsumerStateTable, projectionStateTable, } from "@cosmicdrift/kumiko-framework/pipeline";
@@ -1,13 +1,13 @@
1
- // Drizzle-Schema-Barrel: Minimal (nur Framework-Infra, kein Auth).
1
+ // Minimal Drizzle schema barrel (framework infrastructure only, no auth).
2
2
  //
3
- // Apps OHNE Auth-Stack (anonymous-only, embedded Demos, Headless-APIs)
4
- // brauchen nur die Framework-Infrastructure-Tables — Event-Store +
5
- // Pipeline-State. Verwendung in drizzle/schema.ts:
3
+ // Apps WITHOUT an auth stack (anonymous-only, embedded demos, headless APIs)
4
+ // need only the framework infrastructure tables — event store and
5
+ // pipeline state. Use in drizzle/schema.ts:
6
6
  //
7
7
  // export * from "@cosmicdrift/kumiko-dev-server/drizzle-tables-minimal";
8
- // export * from "./schema.generated"; // App-eigene Entity-Tables
8
+ // export * from "./schema.generated"; // App-specific entity tables
9
9
  //
10
- // Wer Auth-Tables will, nutzt drizzle-tables-auth-mode (umfasst Minimal
11
- // plus Bundle-Tables für Config, Tenant, User, Sessions).
10
+ // To include auth tables, use drizzle-tables-auth-mode (includes Minimal
11
+ // plus bundle tables for config, tenant, user, and sessions).
12
12
  export { archivedStreamsTable, eventsTable, snapshotsTable, upcasterDeadLetterTable, } from "@cosmicdrift/kumiko-framework/event-store";
13
13
  export { eventConsumerStateTable, projectionStateTable, } from "@cosmicdrift/kumiko-framework/pipeline";
@@ -25,7 +25,7 @@ export function mergeExtraContext(base, presets) {
25
25
  const fromBase = typeof base === "function" ? base(deps) : base !== undefined ? { ...base } : {};
26
26
  const merged = { ...fromBase };
27
27
  if (presets.includes("config")) {
28
- // Same precedence as mergeConfigResolverDefault (run-dev-app.ts) — a base-supplied resolver wins, factory always derives from it (fw#3313).
28
+ // Same precedence as mergeConfigResolverDefault (run-dev-app.ts) — a base-supplied resolver wins, factory always derives from it.
29
29
  const configResolver = resolveBaseConfigResolver(fromBase) ?? createConfigResolver();
30
30
  Object.assign(merged, addConfigAccessorFactory({ configResolver }, deps.registry));
31
31
  }
@@ -1,8 +1,8 @@
1
1
  // First-run welcome banner for `runDevApp({ welcomeBanner: true })`.
2
2
  // Opt-in only — `bun dev` should not normalize this for apps that
3
3
  // already log their own startup. Scaffolded apps (create-kumiko-app)
4
- // flip it on so the first `bun dev` doesn't end on "Server läuft" with
5
- // the user wondering where to click.
4
+ // flip it on so the first `bun dev` doesn't end on a bare "server running"
5
+ // line with the user wondering where to click.
6
6
  export function renderWelcomeBanner(input) {
7
7
  const featuresDir = input.featuresDir ?? "src/features/";
8
8
  const docsUrl = input.docsUrl ?? "https://docs.kumiko.rocks/quickstart";