@cosmicdrift/kumiko-dev-server 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 (125) hide show
  1. package/README.md +17 -0
  2. package/bin/kumiko-build.ts +3 -2
  3. package/bin/kumiko-dev.ts +1 -1
  4. package/bin/kumiko-init-deploy.ts +1 -1
  5. package/bin/kumiko-schema-check.ts +4 -1
  6. package/dist/build-server-bundle.d.ts +31 -0
  7. package/dist/build-server-bundle.js +282 -0
  8. package/dist/build.d.ts +2 -0
  9. package/dist/build.js +5 -0
  10. package/dist/cli.d.ts +5 -0
  11. package/dist/cli.js +9 -0
  12. package/dist/codegen/index.d.ts +4 -0
  13. package/dist/codegen/index.js +4 -0
  14. package/dist/codegen/render.d.ts +55 -0
  15. package/dist/codegen/render.js +273 -0
  16. package/dist/codegen/run-codegen.d.ts +20 -0
  17. package/dist/codegen/run-codegen.js +219 -0
  18. package/dist/codegen/scan-events.d.ts +76 -0
  19. package/dist/codegen/scan-events.js +447 -0
  20. package/dist/codegen/watch.d.ts +33 -0
  21. package/dist/codegen/watch.js +112 -0
  22. package/dist/compose-stacks.d.ts +60 -0
  23. package/dist/compose-stacks.js +127 -0
  24. package/dist/crash-tracker.d.ts +17 -0
  25. package/dist/crash-tracker.js +37 -0
  26. package/dist/create-kumiko-server.d.ts +195 -0
  27. package/dist/create-kumiko-server.js +1040 -0
  28. package/dist/env-schema.d.ts +15 -0
  29. package/dist/env-schema.js +60 -0
  30. package/dist/few-shot-corpus.d.ts +72 -0
  31. package/dist/few-shot-corpus.js +262 -0
  32. package/dist/framework-version.d.ts +1 -0
  33. package/dist/framework-version.js +31 -0
  34. package/dist/index.d.ts +22 -0
  35. package/dist/index.js +33 -0
  36. package/dist/init-deploy-cli.d.ts +10 -0
  37. package/dist/init-deploy-cli.js +111 -0
  38. package/dist/kebab.d.ts +1 -0
  39. package/{src/kebab.ts → dist/kebab.js} +2 -3
  40. package/dist/run-dev-app.d.ts +174 -0
  41. package/dist/run-dev-app.js +403 -0
  42. package/dist/scaffold-app-feature.d.ts +16 -0
  43. package/dist/scaffold-app-feature.js +240 -0
  44. package/dist/scaffold-app.d.ts +45 -0
  45. package/dist/scaffold-app.js +852 -0
  46. package/dist/scaffold-demo-tasks.d.ts +8 -0
  47. package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
  48. package/dist/scaffold-deploy.d.ts +92 -0
  49. package/dist/scaffold-deploy.js +254 -0
  50. package/dist/scaffold-feature.d.ts +26 -0
  51. package/dist/scaffold-feature.js +163 -0
  52. package/dist/schema-apply.d.ts +11 -0
  53. package/dist/schema-apply.js +95 -0
  54. package/dist/schema-check-core.d.ts +2 -0
  55. package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
  56. package/dist/schema-tables-auth-mode.d.ts +2 -0
  57. package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
  58. package/dist/schema-tables-minimal.d.ts +2 -0
  59. package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
  60. package/dist/setup-test-stack-from-features.d.ts +11 -0
  61. package/dist/setup-test-stack-from-features.js +42 -0
  62. package/dist/welcome-banner.d.ts +15 -0
  63. package/dist/welcome-banner.js +27 -0
  64. package/package.json +71 -24
  65. package/src/changes.json +6 -0
  66. package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
  67. package/src/__tests__/build-server-bundle.test.ts +0 -228
  68. package/src/__tests__/classify-change.test.ts +0 -87
  69. package/src/__tests__/compose-stacks.test.ts +0 -273
  70. package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
  71. package/src/__tests__/crash-tracker.test.ts +0 -89
  72. package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
  73. package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
  74. package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
  75. package/src/__tests__/discover-format.test.ts +0 -32
  76. package/src/__tests__/env-schema.integration.test.ts +0 -248
  77. package/src/__tests__/env-schema.test.ts +0 -132
  78. package/src/__tests__/few-shot-corpus.test.ts +0 -316
  79. package/src/__tests__/framework-version.test.ts +0 -45
  80. package/src/__tests__/init-deploy-cli.test.ts +0 -113
  81. package/src/__tests__/kumiko-schema-check.test.ts +0 -56
  82. package/src/__tests__/merge-extra-context.test.ts +0 -67
  83. package/src/__tests__/resolve-stylesheet.test.ts +0 -132
  84. package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
  85. package/src/__tests__/run-dev-app.integration.test.ts +0 -290
  86. package/src/__tests__/runtime-classification.test.ts +0 -26
  87. package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
  88. package/src/__tests__/scaffold-app-feature.test.ts +0 -216
  89. package/src/__tests__/scaffold-app.test.ts +0 -630
  90. package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
  91. package/src/__tests__/scaffold-deploy.test.ts +0 -464
  92. package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
  93. package/src/__tests__/scaffold-feature.test.ts +0 -143
  94. package/src/__tests__/schema-apply.integration.test.ts +0 -191
  95. package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
  96. package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
  97. package/src/__tests__/walkthrough.integration.test.ts +0 -140
  98. package/src/__tests__/welcome-banner.test.ts +0 -42
  99. package/src/build-server-bundle.ts +0 -346
  100. package/src/build.ts +0 -28
  101. package/src/codegen/__tests__/render-codegen.test.ts +0 -100
  102. package/src/codegen/__tests__/run-codegen.test.ts +0 -665
  103. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
  104. package/src/codegen/__tests__/watch.test.ts +0 -222
  105. package/src/codegen/index.ts +0 -19
  106. package/src/codegen/render.ts +0 -305
  107. package/src/codegen/run-codegen.ts +0 -278
  108. package/src/codegen/scan-events.ts +0 -565
  109. package/src/codegen/watch.ts +0 -140
  110. package/src/compose-stacks.ts +0 -193
  111. package/src/crash-tracker.ts +0 -56
  112. package/src/create-kumiko-server.ts +0 -1397
  113. package/src/env-schema.ts +0 -72
  114. package/src/few-shot-corpus.ts +0 -370
  115. package/src/framework-version.ts +0 -39
  116. package/src/index.ts +0 -105
  117. package/src/init-deploy-cli.ts +0 -148
  118. package/src/run-dev-app.ts +0 -694
  119. package/src/scaffold-app-feature.ts +0 -283
  120. package/src/scaffold-app.ts +0 -1033
  121. package/src/scaffold-deploy.ts +0 -393
  122. package/src/scaffold-feature.ts +0 -226
  123. package/src/schema-apply.ts +0 -123
  124. package/src/setup-test-stack-from-features.ts +0 -80
  125. package/src/welcome-banner.ts +0 -43
@@ -1,694 +0,0 @@
1
- // High-level dev-server wrapper for sample apps and showcases. In auth mode it
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.
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.
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.
14
-
15
- import {
16
- AuthErrors,
17
- AuthHandlers,
18
- AuthQueries,
19
- } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
20
- import {
21
- type SeedAdminOptions,
22
- seedAdminGuarded,
23
- } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
24
- import {
25
- EXT_SESSION_STORE,
26
- resolveSessionStore,
27
- resolveTokenVerifier,
28
- type SessionStore,
29
- } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
30
- import {
31
- AUTH_MFA_FEATURE,
32
- AuthMfaHandlers,
33
- bindMfaRevokeAllOtherSessionsFromFeature,
34
- bindRevokeAllPatTokensFromFeature,
35
- } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
36
- import {
37
- buildEnvConfigOverrides,
38
- createConfigResolver,
39
- } from "@cosmicdrift/kumiko-bundled-features/config";
40
- import {
41
- bindPatAutoRevokeOnPasswordChangeFromFeature,
42
- PAT_FEATURE,
43
- patRateLimitFromFeature,
44
- revokeAllPatTokensForUser,
45
- } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
46
- import {
47
- bindAutoRevokeFromFeature,
48
- SESSIONS_FEATURE,
49
- } from "@cosmicdrift/kumiko-bundled-features/sessions";
50
- import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
51
- import type {
52
- PostAuthLandingResolver,
53
- SessionMetadata,
54
- TokenVerifier,
55
- } from "@cosmicdrift/kumiko-framework/api";
56
- import {
57
- createInMemoryLoginRateLimiter,
58
- parseTrustedProxyHopsEnv,
59
- TRUSTED_PROXY_HOPS_ENV,
60
- } from "@cosmicdrift/kumiko-framework/api";
61
- import {
62
- configureBlindIndexKey,
63
- configurePiiSubjectKms,
64
- type KmsAdapter,
65
- } from "@cosmicdrift/kumiko-framework/crypto";
66
- import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
67
- import {
68
- collectWriteHandlerQns,
69
- createRegistry,
70
- type EffectiveFeaturesResolver,
71
- type FeatureDefinition,
72
- FILE_STORAGE_PROVIDER_BOOT_SENTINEL,
73
- FILE_STORAGE_PROVIDER_ENV,
74
- findTierResolverUsage,
75
- type Registry,
76
- type SessionUser,
77
- type TenantId,
78
- type TierResolverPlugin,
79
- type ValidateBootOptions,
80
- validateAppCustomScreenWriteQns,
81
- validateBoot,
82
- } from "@cosmicdrift/kumiko-framework/engine";
83
- import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
84
- import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
85
- import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
86
- import type { PageHeadResolver } from "@cosmicdrift/kumiko-headless/apex";
87
- import { applyBootSeeds } from "@cosmicdrift/kumiko-server-runtime/boot/apply-boot-seeds";
88
- import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
89
- import {
90
- buildComposeAuthOptions,
91
- composeFeatures,
92
- } from "@cosmicdrift/kumiko-server-runtime/compose-features";
93
- import { assertPiiBootInvariants } from "@cosmicdrift/kumiko-server-runtime/pii-boot-gate";
94
- import { assertSessionBootInvariants } from "@cosmicdrift/kumiko-server-runtime/session-boot-gate";
95
- import { watchAndRegenerate } from "./codegen";
96
- import {
97
- type CreateKumikoServerOptions,
98
- createKumikoServer,
99
- type KumikoServerHandle,
100
- } from "./create-kumiko-server";
101
- import { renderWelcomeBanner } from "./welcome-banner";
102
-
103
- // Re-export der shared Auth-Setup-Types damit Apps nur einen Import-Pfad
104
- // brauchen. PasswordResetSetup / EmailVerificationSetup leben in
105
- // @cosmicdrift/kumiko-server-runtime (single source of truth) — hier nur
106
- // durchgereicht.
107
- export type {
108
- AccountLockoutSetup,
109
- AccountUnlockSetup,
110
- AuthMailOptions,
111
- EmailVerificationSetup,
112
- InviteSetup,
113
- PasswordResetSetup,
114
- SignupSetup,
115
- } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
116
-
117
- import type {
118
- AccountLockoutSetup,
119
- AccountUnlockSetup,
120
- AuthMailOptions,
121
- EmailVerificationSetup,
122
- InviteSetup,
123
- PasswordResetSetup,
124
- SignupSetup,
125
- } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
126
- import {
127
- addConfigAccessorFactory,
128
- buildBootExtraContext,
129
- requireEnv,
130
- resolveAuthMail,
131
- } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
132
-
133
- export type RunDevAppAuthOptions = {
134
- /** Admin user to seed at boot. Idempotent — re-runs in persistent-DB
135
- * mode reuse the existing user. */
136
- readonly admin: SeedAdminOptions;
137
- /** Optional override of the login error → HTTP status map. Default
138
- * maps invalidCredentials → 401, noMembership → 403. */
139
- readonly loginErrorStatusMap?: Readonly<Record<string, number>>;
140
- /** Auth-Mail-Convenience — symmetrisch zu RunProdAppAuthOptions.mail.
141
- * Verdrahtet alle 4 Mail-Flows aus env-SMTP + Standard-Templates;
142
- * hmacSecret = `JWT_SECRET`-env (Dev-Fallback wenn ungesetzt). Ohne
143
- * `SMTP_HOST`-env bleiben die Flows unverdrahtet (Null-Transport-Guard).
144
- * Explizite Flow-Setups gewinnen über den mail-Default. */
145
- readonly mail?: AuthMailOptions;
146
- /** Password-reset flow. Wenn gesetzt werden /api/auth/request-password-
147
- * reset + /api/auth/reset-password als Public-Routes gemounted UND
148
- * der request/confirm-Handler im auth-email-password-Feature wird
149
- * registriert. Symmetrisch zu RunProdAppAuthOptions.passwordReset. */
150
- readonly passwordReset?: PasswordResetSetup;
151
- /** Email-verification flow. Symmetric to passwordReset. */
152
- readonly emailVerification?: EmailVerificationSetup;
153
- /** Self-signup flow (magic link). Symmetric to RunProdAppAuthOptions. */
154
- readonly signup?: SignupSetup;
155
- /** Tenant-invite flow (magic link). Symmetric. */
156
- readonly invite?: InviteSetup;
157
- /** Account-unlock flow (#1266). Symmetric to RunProdAppAuthOptions. */
158
- readonly accountUnlock?: AccountUnlockSetup;
159
- /** Brute-force guard on the login handler (maxFailedAttempts,
160
- * lockoutDurationMinutes). Mounts no routes. Symmetric to
161
- * RunProdAppAuthOptions.accountLockout (kumiko-framework#1627). */
162
- readonly accountLockout?: AccountLockoutSetup;
163
- /** Domain attribute for both auth cookies (see
164
- * AuthRoutesConfig.cookieDomain). Symmetric to RunProdAppAuthOptions. */
165
- readonly cookieDomain?: string;
166
- /** Server-side Origin allowlist for the CSRF guard (see
167
- * AuthRoutesConfig.allowedOrigins). Symmetric to RunProdAppAuthOptions —
168
- * required once `cookieDomain` is set. */
169
- readonly allowedOrigins?: readonly string[];
170
- /** Opt out of the Origin guard. Symmetric to RunProdAppAuthOptions. */
171
- readonly unsafeSkipOriginCheck?: boolean;
172
- /** Number of trusted reverse-proxy hops for client-IP derivation (see
173
- * AuthRoutesConfig.trustedProxyHops, kumiko-framework#1539). Symmetric to
174
- * RunProdAppAuthOptions — dev usually runs unproxied, so this is normally
175
- * left unset (default 0). */
176
- readonly trustedProxyHops?: number;
177
- /** Server-computed post-auth redirect (see AuthRoutesConfig.postAuthLanding).
178
- * Result lands in the login/signup/invite response as `landingPath`; an
179
- * invalid path or a throwing resolver just omits the field. */
180
- readonly postAuthLanding?: PostAuthLandingResolver;
181
- };
182
-
183
- /** Hook for app-specific seeding (demo data, fixtures). Runs after the
184
- * admin (when auth is active) in declared order. */
185
- export type SeedFn = (stack: TestStack) => Promise<void>;
186
-
187
- export type RunDevAppOptions = {
188
- /** App-spezifische Features. Im auth-mode werden config/user/tenant/
189
- * auth-email-password automatisch dazu gemischt — KEIN doppeltes
190
- * manuelles Hinzufügen nötig. */
191
- readonly features: readonly FeatureDefinition[];
192
- /** Opt-in boot-validator warnings — see ValidateBootOptions. */
193
- readonly validateBootOptions?: ValidateBootOptions;
194
- /** Browser entry module, bundled to /client.js. Mutually exclusive with
195
- * `clientEntries`. */
196
- readonly clientEntry?: string;
197
- /** Multi-entry mode: one bundle (`/client-<name>.js`) and one HTML template
198
- * per entry; `hostDispatch` picks the entry per request. kumiko-build reads
199
- * the same list from package.json `kumiko.clientEntries`. Mutually exclusive
200
- * with `clientEntry`. */
201
- readonly clientEntries?: CreateKumikoServerOptions["clientEntries"];
202
- /** Multi-entry mode: per-request routing. Required with `clientEntries`,
203
- * otherwise the server cannot tell which HTML to serve. */
204
- readonly hostDispatch?: CreateKumikoServerOptions["hostDispatch"];
205
- /** Per-request head-metadata resolver — same option, same signature as
206
- * `runProdApp`'s `resolvePageHead`, so `bun dev` and every e2e that boots
207
- * through `runDevApp` show the same resolved head prod does. See
208
- * `CreateKumikoServerOptions.resolvePageHead` for the full contract. */
209
- readonly resolvePageHead?: PageHeadResolver;
210
- /** CSS-Entry. Default: package-export `@cosmicdrift/kumiko-renderer-web/styles.css`
211
- * wenn ein client-Entry gesetzt ist. `false` deaktiviert die CSS-Pipeline. */
212
- readonly stylesheet?: string | false;
213
- /** Tailwind `--watch` process on/off, see `CreateKumikoServerOptions.stylesheetWatch`. */
214
- readonly stylesheetWatch?: boolean;
215
- /** Eigenes HTML-Template; sonst minimal-Default (#root + client.js).
216
- * Im Multi-Entry-Mode ist es das Fallback-Template, wenn ein einzelner
217
- * Entry kein eigenes htmlPath setzt. */
218
- readonly htmlPath?: string;
219
- /** Listen-Port. Default 4173 (oder $PORT). */
220
- readonly port?: number;
221
- /** Extra-Verzeichnisse für den File-Watcher (Trigger Hot-Reload). */
222
- readonly watchDirs?: readonly string[];
223
- /** SIGINT/SIGTERM-Handler installieren (Default true; in Tests auf
224
- * false damit repeated boots keine Listener akkumulieren). */
225
- readonly installSignalHandlers?: boolean;
226
- /** Auth mode: standard features mixed in, auth routes wired, seedAdminGuarded
227
- * runs in onAfterSetup. Without `auth` the server stays in auto-mint JWT mode. */
228
- readonly auth?: RunDevAppAuthOptions;
229
- /** Eigene Seed-Funktionen, laufen nach dem Admin (wenn auth) in
230
- * Array-Reihenfolge. */
231
- readonly seeds?: readonly SeedFn[];
232
- /** Extra-AppContext-Keys. `templateResolver` (immer) + `secrets` (wenn das
233
- * secrets-Feature gemountet ist) + `configResolver` (auth-mode) werden
234
- * automatisch ergänzt — App-Werte gewinnen. Symmetrisch zu runProdApp. */
235
- /** Boot default locale (BCP-47) for ctx.locale when the request carries
236
- * no locale signal. Merged into AppContext before extraContext (app wins). */
237
- readonly defaultLocale?: string;
238
- readonly extraContext?: CreateKumikoServerOptions["extraContext"];
239
- /** MasterKeyProvider für die auto-verdrahtete `ctx.secrets`. Default:
240
- * `createEnvMasterKeyProvider`. Override für KMS-Backends. Nur relevant
241
- * wenn das secrets-Feature gemountet ist. Symmetrisch zu runProdApp. */
242
- readonly masterKey?: MasterKeyProvider;
243
- /** Subject-Key-KMS für pii-annotierte Felder (Crypto-Shredding) — dev-
244
- * Pendant zu runProdApp({ kms }). Ephemere DB: InMemoryKmsAdapter reicht.
245
- * Persistente Dev-DB: createPgKmsAdapter gegen dieselbe DB, sonst sind
246
- * alte Rows nach dem Restart unlesbar (DEKs weg). Ohne Adapter: Klartext
247
- * + Boot-Warnung. */
248
- readonly kms?: KmsAdapter;
249
- /** 32-Byte-Key (base64) für Blind-Index-HMACs — Pflicht sobald `kms`
250
- * gesetzt ist und lookupable-Felder gemountet sind (sonst Boot-Abbruch,
251
- * symmetrisch zu runProdApp). */
252
- readonly blindIndexKey?: string;
253
- /** Env-Quelle für die ENV→config-app-override-Brücke (Keys mit `env:`
254
- * bekommen ihren env-Wert als app-override-Default — symmetrisch zu
255
- * runProdApp). Default `process.env`. Injizierbar als Test-Seam, damit
256
- * Tests eigene env-Werte reichen statt `process.env` global zu mutieren. */
257
- readonly envSource?: Record<string, string | undefined>;
258
- /** Anonymous-Access für Public-Endpoints — Requests ohne JWT laufen
259
- * als Pseudo-User mit Rolle `anonymous` durch, wenn der Handler die
260
- * Rolle in `access.roles` führt. */
261
- readonly anonymousAccess?: CreateKumikoServerOptions["anonymousAccess"];
262
- /** File-Storage-Provider — aktiviert die Upload-Routes (/api/files) +
263
- * `ctx.files`. Demos: `{ storageProvider: createInMemoryFileProvider() }`. */
264
- readonly files?: CreateKumikoServerOptions["files"];
265
- /** App-eigene HTTP-Routes (z.B. /feed.xml, /sitemap.xml) — wird ans
266
- * Hono-app gehängt, läuft VOR dem static-asset-Pfad. Symmetrisch zur
267
- * gleichnamigen Option in runProdApp. */
268
- readonly extraRoutes?: CreateKumikoServerOptions["extraRoutes"];
269
- /** Hook for app-wired co-running components — symmetric to
270
- * runProdApp.wire (kumiko-framework#3050). */
271
- readonly wire?: CreateKumikoServerOptions["wire"];
272
- /** Feature-toggle resolver — durchgereicht an createKumikoServer →
273
- * setupTestStack. Sprint-8 Tier-Composition: per-Tenant unterschied-
274
- * liche features aktiv via globalFeatureToggleRuntime. Pattern in
275
- * bin/server.ts: createLateBoundHolder + post-boot runtime.initialize
276
- * in einem seed-fn, weil die runtime stack.db braucht und die seed-
277
- * Funktionen nach setupTestStack laufen. */
278
- readonly effectiveFeatures?: CreateKumikoServerOptions["effectiveFeatures"];
279
- /** Print a first-run banner after the server starts (URL, admin login,
280
- * hot-reload hint, docs link). Default: off — apps that already log
281
- * their own startup shouldn't get double-printed. The scaffold template
282
- * (create-kumiko-app) flips this on so the first `bun dev` ends on
283
- * something the user can click. Pass an object to override the
284
- * features-dir hint or the docs URL. */
285
- readonly welcomeBanner?: boolean | { readonly featuresDir?: string; readonly docsUrl?: string };
286
- /** Forwarded to createKumikoServer → setupTestStack's top-level
287
- * `trustedProxyHops` — symmetric to
288
- * RunProdAppOptions.trustedProxyHops. Wins over the deprecated
289
- * `auth.trustedProxyHops`, which wins over the `KUMIKO_TRUSTED_PROXY_HOPS`
290
- * env var. Dev usually runs unproxied, so this is normally left unset
291
- * (default 0). */
292
- readonly trustedProxyHops?: number;
293
- };
294
-
295
- export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServerHandle> {
296
- // Auto-mix Standard-Features im auth-mode via composeFeatures (single
297
- // source of truth — auch runProdApp und der per-app drizzle-Schema-
298
- // Generator nutzen denselben Helper, damit Migration und Runtime nie
299
- // auseinanderdriften können).
300
- const envSource = options.envSource ?? process.env;
301
- // auth.mail → normalisiert in die expliziten Flow-Felder (symmetrisch zu
302
- // runProdApp). hmacSecret = JWT_SECRET-env (fail-fast, symmetrisch zu
303
- // runProdApp). Ab hier IMMER effectiveAuth statt options.auth.
304
- const effectiveAuth = options.auth
305
- ? resolveAuthMail(options.auth, requireEnv("JWT_SECRET", envSource, "runDevApp"), envSource)
306
- : undefined;
307
- const composeAuthOptions = buildComposeAuthOptions(effectiveAuth);
308
- const features = composeFeatures(options.features, {
309
- includeBundled: !!effectiveAuth,
310
- ...(composeAuthOptions && { authOptions: composeAuthOptions }),
311
- });
312
-
313
- // An explicitly wired file provider (options.files) satisfies the
314
- // FILE_STORAGE_PROVIDER boot gate — set it before validateBoot runs. Only
315
- // if WE set it (532/2): deleting it after use instead of leaving a
316
- // permanent process.env mutation, so a second runDevApp call in the same
317
- // process (no files this time) doesn't fall through the gate falsely.
318
- const setFileStorageProviderEnv =
319
- options.files !== undefined && process.env[FILE_STORAGE_PROVIDER_ENV] === undefined;
320
- if (setFileStorageProviderEnv) {
321
- process.env[FILE_STORAGE_PROVIDER_ENV] = FILE_STORAGE_PROVIDER_BOOT_SENTINEL;
322
- }
323
-
324
- // Boot-Validation als allererstes — vor fs-Watcher und Server. Dieselbe
325
- // Fehlerklasse (unqualifizierte nav-/handler-QNs, screen-access etc.),
326
- // die früher nur runProdApp fing und sonst erst den Prod-Pod im
327
- // CrashLoopBackOff sterben ließ (#359). Wirft synchron, bevor ein
328
- // Socket oder Watcher (codegen-Write) aufgeht.
329
- try {
330
- validateBoot(features, options.validateBootOptions);
331
- } finally {
332
- if (setFileStorageProviderEnv) delete process.env[FILE_STORAGE_PROVIDER_ENV];
333
- }
334
- warnIfNonUtcServerTimeZone();
335
- validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
336
-
337
- // Codegen + File-Watcher — schreibt `<appRoot>/.kumiko/types.generated.d.ts`
338
- // + `define.ts` aus den r.defineEvent-Aufrufen der App, einmal beim
339
- // Boot UND danach bei jeder relevanten Änderung unter `<appRoot>/src/`.
340
- // Idempotent (writeIfChanged) — der TS-Sprachserver kriegt nur einen
341
- // Reload-Tick wenn sich tatsächlich was geändert hat.
342
- //
343
- // App-Root ist process.cwd() (bun-dev läuft vom App-Workspace). Der
344
- // Watcher läuft solange der Dev-Server lebt; close() bei Shutdown
345
- // wird über das createKumikoServer-Handle implizit erledigt (Bun's
346
- // process-exit räumt fs.watch-handles auf).
347
- // Sammelt alle Write-Handler-QNs für den Codegen — damit
348
- // `types.generated.d.ts` eine `WriteHandlerQn`-Union exportieren
349
- // kann, die dispatcher.write-Aufrufe client-seitig typisiert.
350
- const handlerQns = collectWriteHandlerQns(features);
351
- watchAndRegenerate({ appRoot: process.cwd(), handlerQns: [...handlerQns] });
352
-
353
- // Sprint-8a Tier-Composition auto-wire: scan features for a
354
- // tenantTierResolver-extension. If found AND user didn't supply own
355
- // effectiveFeatures, we wire a late-bound wrapper here and fill it
356
- // in onAfterSetup (where stack.db + stack.registry are available).
357
- // App-Author sees nothing — `createTierEngineFeature(opts)` mounts +
358
- // framework auto-wires.
359
- const tierResolverUsage = options.effectiveFeatures ? undefined : findTierResolverUsage(features);
360
- const tierResolverHolder: { resolver: EffectiveFeaturesResolver | undefined } = {
361
- resolver: undefined,
362
- };
363
- const finalEffectiveFeatures: EffectiveFeaturesResolver | undefined =
364
- options.effectiveFeatures ??
365
- (tierResolverUsage
366
- ? Object.assign(
367
- (tenantId: TenantId) => {
368
- // Defensive: Server starts AFTER onAfterSetup completes, so the
369
- // resolver is filled before any request comes in. Throwing here
370
- // means a programming error (boot order) rather than silent
371
- // "all-features-on" misbehavior.
372
- if (!tierResolverHolder.resolver) {
373
- throw new Error(
374
- "tier-resolver: extension found but resolver not yet built — boot order issue?",
375
- );
376
- }
377
- return tierResolverHolder.resolver(tenantId);
378
- },
379
- {
380
- // Late-bind: der echte trialGate hängt erst nach plugin.build() am
381
- // holder.resolver. Delegieren (nicht kopieren), weil dieses
382
- // Wrapper-Closure vor build() konstruiert wird. Kein Trial → false.
383
- trialGate: (tenantId: TenantId, featureName: string): Promise<boolean> =>
384
- tierResolverHolder.resolver?.trialGate?.(tenantId, featureName) ??
385
- Promise.resolve(false),
386
- },
387
- )
388
- : undefined);
389
-
390
- // configResolver-default fürs config-feature — im auth-mode immer
391
- // hinzufügen, im no-auth-mode dem Caller überlassen. Factory-form
392
- // wird gewrap't damit der spread auf das aufgerufene Result greift,
393
- // nicht auf die function selbst (no-op). Die ENV→config-Brücke (Keys mit
394
- // `env:` → app-override-Default) läuft symmetrisch zu runProdApp; die
395
- // throwaway-Registry hier extrahiert nur die config-Keys, weil der
396
- // configResolver vor dem Server-Boot konstruiert wird (stack.registry
397
- // gibt's erst onAfterSetup) — createKumikoServer baut intern seine eigene.
398
- const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
399
- // App-wide cipher for `encrypted: true` entity fields (symmetrisch zu
400
- // runProdApp) — executors resolve it lazily.
401
- configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
402
- // Subject-KMS + Blind-Index (symmetrisch zu runProdApp). Dev warnt bei
403
- // Klartext-PII statt zu failen; kms+lookupable ohne blindIndexKey bricht
404
- // auch hier (Lookups wären in jedem Modus kaputt).
405
- configurePiiSubjectKms(options.kms);
406
- configureBlindIndexKey(options.blindIndexKey);
407
- assertPiiBootInvariants(features, {
408
- kms: options.kms,
409
- blindIndexKey: options.blindIndexKey,
410
- mode: "dev",
411
- });
412
- // Throwaway registry, built once and reused below — mirrors runProdApp's
413
- // `createRegistry(features)` right after validateBoot (#2027). Symmetric
414
- // sessionStoreProviderMounted check to runProdApp's (registry.getExtension-
415
- // Usages, not a raw features.some(...) scan) so dev and prod never ask the
416
- // same question two different ways again.
417
- const registry = effectiveAuth ? createRegistry(features) : undefined;
418
- const sessionStoreProviderMounted =
419
- registry !== undefined && registry.getExtensionUsages(EXT_SESSION_STORE).length > 0;
420
- assertSessionBootInvariants({
421
- hasAuth: Boolean(effectiveAuth),
422
- sessionStoreProviderMounted,
423
- mode: "dev",
424
- });
425
- const cfgExtra = registry
426
- ? mergeConfigResolverDefault(options.extraContext, registry, envSource, bootCrypto.configCipher)
427
- : options.extraContext;
428
- // Auto-wire templateResolver (immer) + secrets (feature-gated), symmetrisch zu
429
- // runProdApp. Anders als prod existiert die db hier erst im Factory-deps
430
- // (createKumikoServer baut den Stack), darum als Factory: buildBootExtraContext
431
- // mit hasAuth:false (configResolver kommt schon aus cfgExtra), App-Werte
432
- // (cfgExtra) gewinnen über die Boot-Defaults.
433
- const extraContext: CreateKumikoServerOptions["extraContext"] = (deps) => {
434
- const boot = buildBootExtraContext({
435
- db: deps.db,
436
- features,
437
- envSource,
438
- registry: deps.registry,
439
- hasAuth: false,
440
- sseBroker: deps.sseBroker,
441
- crypto: bootCrypto,
442
- });
443
- const base = typeof cfgExtra === "function" ? cfgExtra(deps) : (cfgExtra ?? {});
444
- return {
445
- ...boot,
446
- ...(options.defaultLocale !== undefined && { defaultLocale: options.defaultLocale }),
447
- ...base,
448
- };
449
- };
450
-
451
- // Sessions: late-bound holder — resolveSessionStore needs concrete db+
452
- // registry (only after setupTestStack). Wired when sessions feature is
453
- // mounted (#1372); no auth.sessions opt-in anymore.
454
- let sessionStore: SessionStore | undefined;
455
- const requireSessionStore = (): SessionStore => {
456
- if (!sessionStore) {
457
- throw new Error("[runDevApp] sessionStore accessed before onAfterSetup");
458
- }
459
- return sessionStore;
460
- };
461
- // Token-verifier opt-in: same late-bound holder pattern — resolveTokenVerifier
462
- // needs the real db+registry (only concrete after setupTestStack). Wired
463
- // whenever a provider feature (personal-access-tokens today) is mounted.
464
- let tokenVerifier: TokenVerifier | undefined;
465
- const patFeature = features.find((f) => f.name === PAT_FEATURE);
466
- const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
467
- const patAuthFragment = patFeature
468
- ? {
469
- tokenVerifier: (rawToken: string) => {
470
- if (!tokenVerifier) {
471
- throw new Error("[runDevApp] token-verifier accessed before onAfterSetup");
472
- }
473
- return tokenVerifier(rawToken);
474
- },
475
- patRateLimiter: (() => {
476
- const rl = patRateLimitFromFeature(patFeature);
477
- return createInMemoryLoginRateLimiter(rl.maxRequests, rl.windowMs);
478
- })(),
479
- }
480
- : {};
481
-
482
- // Parity with runProdApp (#1372 review): gate on sessionStore provider, not
483
- // the sessions feature name — a custom provider must also wire auth
484
- // callbacks. sessionStoreProviderMounted computed above via the registry
485
- // (same check as runProdApp, #2027) — not recomputed here.
486
- const sessionAuthFragment =
487
- effectiveAuth && sessionStoreProviderMounted
488
- ? {
489
- sessionCreator: (user: SessionUser, meta: SessionMetadata) =>
490
- requireSessionStore().creator(user, meta),
491
- sessionRevoker: (sid: string) => requireSessionStore().revoker(sid),
492
- sessionChecker: (sid: string, userId: string) =>
493
- requireSessionStore().checker(sid, userId),
494
- }
495
- : {};
496
-
497
- const trustedProxyHops =
498
- options.trustedProxyHops ??
499
- effectiveAuth?.trustedProxyHops ??
500
- parseTrustedProxyHopsEnv(envSource[TRUSTED_PROXY_HOPS_ENV], "runDevApp");
501
-
502
- const handle = await createKumikoServer({
503
- features,
504
- ...(options.clientEntry !== undefined && { clientEntry: options.clientEntry }),
505
- ...(options.clientEntries !== undefined && { clientEntries: options.clientEntries }),
506
- ...(options.hostDispatch !== undefined && { hostDispatch: options.hostDispatch }),
507
- ...(options.resolvePageHead !== undefined && { resolvePageHead: options.resolvePageHead }),
508
- ...(options.stylesheet !== undefined && { stylesheet: options.stylesheet }),
509
- ...(options.stylesheetWatch !== undefined && { stylesheetWatch: options.stylesheetWatch }),
510
- ...(options.htmlPath !== undefined && { htmlPath: options.htmlPath }),
511
- ...(options.port !== undefined && { port: options.port }),
512
- ...(options.watchDirs !== undefined && { watchDirs: options.watchDirs }),
513
- ...(options.installSignalHandlers !== undefined && {
514
- installSignalHandlers: options.installSignalHandlers,
515
- }),
516
- ...(extraContext !== undefined && { extraContext }),
517
- ...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
518
- ...(options.files !== undefined && { files: options.files }),
519
- ...(options.extraRoutes !== undefined && { extraRoutes: options.extraRoutes }),
520
- ...(options.wire !== undefined && { wire: options.wire }),
521
- ...(trustedProxyHops !== undefined && { trustedProxyHops }),
522
- ...(finalEffectiveFeatures !== undefined && {
523
- effectiveFeatures: finalEffectiveFeatures,
524
- }),
525
- ...(effectiveAuth && {
526
- auth: {
527
- membershipQuery: TenantQueries.memberships,
528
- loginHandler: AuthHandlers.login,
529
- loginErrorStatusMap: effectiveAuth.loginErrorStatusMap ?? {
530
- [AuthErrors.invalidCredentials]: 401,
531
- [AuthErrors.noMembership]: 403,
532
- },
533
- ...(effectiveAuth.cookieDomain !== undefined && {
534
- cookieDomain: effectiveAuth.cookieDomain,
535
- }),
536
- ...(effectiveAuth.allowedOrigins !== undefined && {
537
- allowedOrigins: effectiveAuth.allowedOrigins,
538
- }),
539
- ...(effectiveAuth.unsafeSkipOriginCheck !== undefined && {
540
- unsafeSkipOriginCheck: effectiveAuth.unsafeSkipOriginCheck,
541
- }),
542
- ...(effectiveAuth.trustedProxyHops !== undefined && {
543
- trustedProxyHops: effectiveAuth.trustedProxyHops,
544
- }),
545
- ...(effectiveAuth.postAuthLanding !== undefined && {
546
- postAuthLanding: effectiveAuth.postAuthLanding,
547
- }),
548
- ...sessionAuthFragment,
549
- ...patAuthFragment,
550
- ...(mfaFeature && {
551
- mfaVerifyHandler: AuthMfaHandlers.verify,
552
- mfaPreauthEnableStartHandler: AuthMfaHandlers.enableStartPreauth,
553
- mfaPreauthConfirmHandler: AuthMfaHandlers.enableConfirmPreauth,
554
- }),
555
- ...(effectiveAuth.passwordReset && {
556
- passwordReset: {
557
- requestHandler: AuthHandlers.requestPasswordReset,
558
- confirmHandler: AuthHandlers.resetPassword,
559
- },
560
- }),
561
- ...(effectiveAuth.emailVerification && {
562
- emailVerification: {
563
- requestHandler: AuthHandlers.requestEmailVerification,
564
- confirmHandler: AuthHandlers.verifyEmail,
565
- },
566
- }),
567
- ...(effectiveAuth.accountUnlock && {
568
- accountUnlock: {
569
- requestHandler: AuthHandlers.requestAccountUnlock,
570
- confirmHandler: AuthHandlers.confirmAccountUnlock,
571
- },
572
- }),
573
- ...(effectiveAuth.signup && {
574
- signup: {
575
- requestHandler: AuthHandlers.signupRequest,
576
- confirmHandler: AuthHandlers.signupConfirm,
577
- },
578
- }),
579
- ...(effectiveAuth.invite && {
580
- invite: {
581
- acceptHandler: AuthHandlers.inviteAccept,
582
- acceptWithLoginHandler: AuthHandlers.inviteAcceptWithLogin,
583
- signupCompleteHandler: AuthHandlers.inviteSignupComplete,
584
- infoHandler: AuthQueries.inviteInfo,
585
- },
586
- }),
587
- },
588
- }),
589
- onAfterSetup: async (stack) => {
590
- // Sprint-8a: build tier-resolver BEFORE any seeds so seeds can rely
591
- // on the resolver being live (e.g. seed that writes a SystemAdmin's
592
- // tier-assignment can immediately read tier-cuts).
593
- if (tierResolverUsage) {
594
- const plugin = tierResolverUsage.options as TierResolverPlugin;
595
- tierResolverHolder.resolver = await plugin.build({
596
- db: stack.db,
597
- registry: stack.registry,
598
- });
599
- }
600
- if (effectiveAuth && stack.registry.getExtensionUsages(EXT_SESSION_STORE).length > 0) {
601
- // Secure-by-default (#1372): resolve sessionStore provider; bind
602
- // password-change mass-revoke + MFA revokeAllOthers.
603
- const store = await resolveSessionStore({
604
- db: stack.db,
605
- registry: stack.registry,
606
- });
607
- const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
608
- if (sessionsFeature) {
609
- bindAutoRevokeFromFeature(sessionsFeature)?.(store.massRevoker);
610
- }
611
- if (mfaFeature) {
612
- bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(store.revokeAllOthers);
613
- }
614
- if (mfaFeature && patFeature) {
615
- bindRevokeAllPatTokensFromFeature(mfaFeature)?.((userId) =>
616
- revokeAllPatTokensForUser(stack.db, userId),
617
- );
618
- }
619
- sessionStore = store;
620
- }
621
- if (patFeature) {
622
- tokenVerifier = (rawToken) =>
623
- resolveTokenVerifier({ db: stack.db, registry: stack.registry }, rawToken);
624
- // Password-change → PAT-revoke: independent of sessions/MFA being
625
- // mounted, so wired unconditionally here whenever PAT itself is.
626
- bindPatAutoRevokeOnPasswordChangeFromFeature(patFeature)?.((userId) =>
627
- revokeAllPatTokensForUser(stack.db, userId),
628
- );
629
- }
630
- if (effectiveAuth) {
631
- await seedAdminGuarded(stack.db, effectiveAuth.admin);
632
- }
633
- // Apply r.config({ seeds }) declared by any registered feature.
634
- // Runs before user-supplied seed callbacks so those can read /
635
- // override the deploy-defaults. The helper indirection is what
636
- // config-seed-boot.integration.ts pins — keep it as a single call.
637
- await applyBootSeeds({
638
- registry: stack.registry,
639
- db: stack.db,
640
- ...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
641
- });
642
- for (const seed of options.seeds ?? []) {
643
- await seed(stack);
644
- }
645
- },
646
- });
647
-
648
- if (options.welcomeBanner) {
649
- const overrides = typeof options.welcomeBanner === "object" ? options.welcomeBanner : {};
650
- const port = handle.server?.port ?? options.port ?? 3000;
651
- const banner = renderWelcomeBanner({
652
- url: `http://localhost:${port}`,
653
- ...(effectiveAuth?.admin && {
654
- admin: {
655
- email: effectiveAuth.admin.email,
656
- password: effectiveAuth.admin.password,
657
- },
658
- }),
659
- ...(overrides.featuresDir !== undefined && { featuresDir: overrides.featuresDir }),
660
- ...(overrides.docsUrl !== undefined && { docsUrl: overrides.docsUrl }),
661
- });
662
- // biome-ignore lint/suspicious/noConsole: boot-time UX print, opt-in via welcomeBanner.
663
- console.log(`\n${banner}\n`);
664
- }
665
-
666
- return handle;
667
- }
668
-
669
- // Exported for the wiring-contract test (config-resolver-default.integration):
670
- // pins that the dev configResolver-default carries the ENV→app-override bridge
671
- // and that a caller-supplied configResolver still overrides it. Not re-exported
672
- // from the package barrel (index.ts lists only runDevApp).
673
- export function mergeConfigResolverDefault(
674
- ctx: CreateKumikoServerOptions["extraContext"],
675
- registry: Registry,
676
- envSource: Record<string, string | undefined>,
677
- cipher?: EnvelopeCipher,
678
- ): CreateKumikoServerOptions["extraContext"] {
679
- const defaults = {
680
- configResolver: createConfigResolver({
681
- appOverrides: buildEnvConfigOverrides(registry, envSource),
682
- ...(cipher && { cipher }),
683
- }),
684
- };
685
- // ctx.config wird per-Request aus _configAccessorFactory geminted (siehe
686
- // addConfigAccessorFactory) — sonst bleibt ctx.config undefined und Handler
687
- // die es lesen (createFileProviderForTenant) werfen. Aus dem EFFEKTIVEN
688
- // Resolver (Caller-Override gewinnt) gebaut, symmetrisch zu runProdApp.
689
- if (ctx === undefined) return addConfigAccessorFactory(defaults, registry);
690
- if (typeof ctx === "function") {
691
- return (deps) => addConfigAccessorFactory({ ...defaults, ...ctx(deps) }, registry);
692
- }
693
- return addConfigAccessorFactory({ ...defaults, ...ctx }, registry);
694
- }