@cosmicdrift/kumiko-dev-server 1.0.0 → 2.0.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.
- package/bin/kumiko-schema-check.ts +7 -0
- package/package.json +12 -7
- package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
- package/src/__tests__/build-server-bundle.test.ts +62 -0
- package/src/__tests__/compose-stacks.test.ts +271 -0
- package/src/__tests__/create-kumiko-server-errors.test.ts +54 -0
- package/src/__tests__/create-kumiko-server-options.test.ts +33 -0
- package/src/__tests__/create-kumiko-server.integration.test.ts +128 -1
- package/src/__tests__/discover-format.test.ts +1 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/few-shot-corpus.test.ts +16 -11
- package/src/__tests__/merge-extra-context.test.ts +56 -0
- package/src/__tests__/resolve-stylesheet.test.ts +16 -0
- package/src/__tests__/saas-identity-wire.integration.test.ts +430 -0
- package/src/__tests__/scaffold-app-feature.test.ts +59 -6
- package/src/__tests__/scaffold-app.test.ts +34 -2
- package/src/__tests__/schema-apply.integration.test.ts +5 -2
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +30 -0
- package/src/__tests__/walkthrough.integration.test.ts +22 -6
- package/src/build-server-bundle.ts +33 -15
- package/src/build.ts +1 -2
- package/src/codegen/__tests__/render-codegen.test.ts +2 -1
- package/src/codegen/__tests__/run-codegen.test.ts +2 -2
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +6 -1
- package/src/codegen/__tests__/watch.test.ts +1 -2
- package/src/codegen/render.ts +6 -1
- package/src/codegen/scan-events.ts +7 -5
- package/src/codegen/watch.ts +7 -4
- package/src/compose-stacks.ts +184 -0
- package/src/create-kumiko-server.ts +28 -5
- package/src/few-shot-corpus.ts +4 -3
- package/src/index.ts +30 -12
- package/src/run-dev-app.ts +195 -61
- package/src/scaffold-app-feature.ts +118 -15
- package/src/scaffold-app.ts +151 -79
- package/src/scaffold-demo-tasks.ts +233 -0
- package/src/schema-apply.ts +15 -2
- package/src/schema-check-core.ts +1 -1
- package/src/setup-test-stack-from-features.ts +61 -0
- package/src/welcome-banner.ts +1 -4
- package/src/__tests__/boot-extra-context.test.ts +0 -140
- package/src/__tests__/build-prod-bundle.test.ts +0 -311
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
- package/src/__tests__/compose-features.test.ts +0 -129
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/inject-schema.test.ts +0 -62
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/require-env.test.ts +0 -29
- package/src/__tests__/resolve-auth-mail.test.ts +0 -69
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
- package/src/__tests__/run-prod-app-spec.test.ts +0 -57
- package/src/__tests__/run-prod-app.integration.test.ts +0 -840
- package/src/__tests__/session-wiring.test.ts +0 -51
- package/src/__tests__/try-hono-first.test.ts +0 -63
- package/src/boot/apply-boot-seeds.ts +0 -18
- package/src/build-prod-bundle.ts +0 -697
- package/src/compose-features.ts +0 -145
- package/src/extra-routes-deps.ts +0 -47
- package/src/inject-schema.ts +0 -24
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app.ts +0 -1492
- package/src/session-wiring.ts +0 -29
- package/src/try-hono-first.ts +0 -46
package/src/run-dev-app.ts
CHANGED
|
@@ -18,16 +18,43 @@ import {
|
|
|
18
18
|
type SeedAdminOptions,
|
|
19
19
|
seedAdmin,
|
|
20
20
|
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
21
|
+
import {
|
|
22
|
+
EXT_SESSION_STORE,
|
|
23
|
+
resolveSessionStore,
|
|
24
|
+
resolveTokenVerifier,
|
|
25
|
+
type SessionStore,
|
|
26
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
27
|
+
import {
|
|
28
|
+
AUTH_MFA_FEATURE,
|
|
29
|
+
AuthMfaHandlers,
|
|
30
|
+
bindMfaRevokeAllOtherSessionsFromFeature,
|
|
31
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
21
32
|
import {
|
|
22
33
|
buildEnvConfigOverrides,
|
|
23
34
|
createConfigResolver,
|
|
24
35
|
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
25
36
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
PAT_FEATURE,
|
|
38
|
+
patRateLimitFromFeature,
|
|
39
|
+
} from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
40
|
+
import {
|
|
41
|
+
bindAutoRevokeFromFeature,
|
|
42
|
+
SESSIONS_FEATURE,
|
|
28
43
|
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
29
44
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
30
|
-
import
|
|
45
|
+
import {
|
|
46
|
+
resolveTenantLifecycleGate,
|
|
47
|
+
TENANT_LIFECYCLE_FEATURE,
|
|
48
|
+
} from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
|
|
49
|
+
import type { SessionMetadata, TokenVerifier } from "@cosmicdrift/kumiko-framework/api";
|
|
50
|
+
import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
|
|
51
|
+
import {
|
|
52
|
+
configureBlindIndexKey,
|
|
53
|
+
configurePiiSubjectKms,
|
|
54
|
+
type KmsAdapter,
|
|
55
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
56
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
57
|
+
import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
31
58
|
import {
|
|
32
59
|
collectWriteHandlerQns,
|
|
33
60
|
createRegistry,
|
|
@@ -41,13 +68,17 @@ import {
|
|
|
41
68
|
validateAppCustomScreenWriteQns,
|
|
42
69
|
validateBoot,
|
|
43
70
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
44
|
-
import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
71
|
+
import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
45
72
|
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
46
73
|
import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
47
|
-
import { applyBootSeeds } from "
|
|
48
|
-
|
|
74
|
+
import { applyBootSeeds } from "@cosmicdrift/kumiko-server-runtime/boot/apply-boot-seeds";
|
|
75
|
+
import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
|
|
76
|
+
import {
|
|
77
|
+
buildComposeAuthOptions,
|
|
78
|
+
composeFeatures,
|
|
79
|
+
} from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
80
|
+
import { assertPiiBootInvariants } from "@cosmicdrift/kumiko-server-runtime/pii-boot-gate";
|
|
49
81
|
import { watchAndRegenerate } from "./codegen";
|
|
50
|
-
import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
|
|
51
82
|
import {
|
|
52
83
|
type CreateKumikoServerOptions,
|
|
53
84
|
createKumikoServer,
|
|
@@ -57,28 +88,31 @@ import { renderWelcomeBanner } from "./welcome-banner";
|
|
|
57
88
|
|
|
58
89
|
// Re-export der shared Auth-Setup-Types damit Apps nur einen Import-Pfad
|
|
59
90
|
// brauchen. PasswordResetSetup / EmailVerificationSetup leben in
|
|
60
|
-
//
|
|
91
|
+
// @cosmicdrift/kumiko-server-runtime (single source of truth) — hier nur
|
|
92
|
+
// durchgereicht.
|
|
61
93
|
export type {
|
|
94
|
+
AccountUnlockSetup,
|
|
62
95
|
AuthMailOptions,
|
|
63
96
|
EmailVerificationSetup,
|
|
64
97
|
InviteSetup,
|
|
65
98
|
PasswordResetSetup,
|
|
66
99
|
SignupSetup,
|
|
67
|
-
} from "
|
|
100
|
+
} from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|
|
68
101
|
|
|
69
102
|
import type {
|
|
103
|
+
AccountUnlockSetup,
|
|
70
104
|
AuthMailOptions,
|
|
71
105
|
EmailVerificationSetup,
|
|
72
106
|
InviteSetup,
|
|
73
107
|
PasswordResetSetup,
|
|
74
108
|
SignupSetup,
|
|
75
|
-
} from "
|
|
109
|
+
} from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|
|
76
110
|
import {
|
|
77
111
|
addConfigAccessorFactory,
|
|
78
112
|
buildBootExtraContext,
|
|
79
113
|
requireEnv,
|
|
80
114
|
resolveAuthMail,
|
|
81
|
-
} from "
|
|
115
|
+
} from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|
|
82
116
|
|
|
83
117
|
export type RunDevAppAuthOptions = {
|
|
84
118
|
/** Admin user to seed at boot. Idempotent — re-runs in persistent-DB
|
|
@@ -87,17 +121,6 @@ export type RunDevAppAuthOptions = {
|
|
|
87
121
|
/** Optional override of the login error → HTTP status map. Default
|
|
88
122
|
* maps invalidCredentials → 401, noMembership → 403. */
|
|
89
123
|
readonly loginErrorStatusMap?: Readonly<Record<string, number>>;
|
|
90
|
-
/** Opt-in: revocable server-side sessions. Caller MUSS
|
|
91
|
-
* `createSessionsFeature()` zu `features` adden — runDevApp wired
|
|
92
|
-
* hier nur die Auth-Callbacks (creator/revoker/checker) gegen
|
|
93
|
-
* stack.db, plus sessionStrictMode=true.
|
|
94
|
-
*
|
|
95
|
-
* Standardverhalten ohne diese Option: stateless JWTs ohne sid,
|
|
96
|
-
* Logout ist client-side cookie-clear, Kartenhaus existing-Apps
|
|
97
|
-
* bleibt unangefasst. */
|
|
98
|
-
readonly sessions?: {
|
|
99
|
-
readonly expiresInMs?: number;
|
|
100
|
-
};
|
|
101
124
|
/** Auth-Mail-Convenience — symmetrisch zu RunProdAppAuthOptions.mail.
|
|
102
125
|
* Verdrahtet alle 4 Mail-Flows aus env-SMTP + Standard-Templates;
|
|
103
126
|
* hmacSecret = `JWT_SECRET`-env (Dev-Fallback wenn ungesetzt). Ohne
|
|
@@ -109,21 +132,28 @@ export type RunDevAppAuthOptions = {
|
|
|
109
132
|
* der request/confirm-Handler im auth-email-password-Feature wird
|
|
110
133
|
* registriert. Symmetrisch zu RunProdAppAuthOptions.passwordReset. */
|
|
111
134
|
readonly passwordReset?: PasswordResetSetup;
|
|
112
|
-
/** Email-verification flow. Symmetric
|
|
135
|
+
/** Email-verification flow. Symmetric to passwordReset. */
|
|
113
136
|
readonly emailVerification?: EmailVerificationSetup;
|
|
114
|
-
/** Self-
|
|
137
|
+
/** Self-signup flow (magic link). Symmetric to RunProdAppAuthOptions. */
|
|
115
138
|
readonly signup?: SignupSetup;
|
|
116
|
-
/** Tenant-
|
|
139
|
+
/** Tenant-invite flow (magic link). Symmetric. */
|
|
117
140
|
readonly invite?: InviteSetup;
|
|
141
|
+
/** Account-unlock flow (#1266). Symmetric to RunProdAppAuthOptions. */
|
|
142
|
+
readonly accountUnlock?: AccountUnlockSetup;
|
|
118
143
|
/** Domain attribute for both auth cookies (see
|
|
119
|
-
* AuthRoutesConfig.cookieDomain). Symmetric
|
|
144
|
+
* AuthRoutesConfig.cookieDomain). Symmetric to RunProdAppAuthOptions. */
|
|
120
145
|
readonly cookieDomain?: string;
|
|
121
146
|
/** Server-side Origin allowlist for the CSRF guard (see
|
|
122
|
-
* AuthRoutesConfig.allowedOrigins). Symmetric
|
|
147
|
+
* AuthRoutesConfig.allowedOrigins). Symmetric to RunProdAppAuthOptions —
|
|
123
148
|
* required once `cookieDomain` is set. */
|
|
124
149
|
readonly allowedOrigins?: readonly string[];
|
|
125
|
-
/** Opt out of the Origin guard. Symmetric
|
|
150
|
+
/** Opt out of the Origin guard. Symmetric to RunProdAppAuthOptions. */
|
|
126
151
|
readonly unsafeSkipOriginCheck?: boolean;
|
|
152
|
+
/** Number of trusted reverse-proxy hops for client-IP derivation (see
|
|
153
|
+
* AuthRoutesConfig.trustedProxyHops, kumiko-framework#1539). Symmetric to
|
|
154
|
+
* RunProdAppAuthOptions — dev usually runs unproxied, so this is normally
|
|
155
|
+
* left unset (default 0). */
|
|
156
|
+
readonly trustedProxyHops?: number;
|
|
127
157
|
};
|
|
128
158
|
|
|
129
159
|
/** Hook for app-specific seeding (demo data, fixtures). Runs after the
|
|
@@ -176,6 +206,16 @@ export type RunDevAppOptions = {
|
|
|
176
206
|
* `createEnvMasterKeyProvider`. Override für KMS-Backends. Nur relevant
|
|
177
207
|
* wenn das secrets-Feature gemountet ist. Symmetrisch zu runProdApp. */
|
|
178
208
|
readonly masterKey?: MasterKeyProvider;
|
|
209
|
+
/** Subject-Key-KMS für pii-annotierte Felder (Crypto-Shredding) — dev-
|
|
210
|
+
* Pendant zu runProdApp({ kms }). Ephemere DB: InMemoryKmsAdapter reicht.
|
|
211
|
+
* Persistente Dev-DB: createPgKmsAdapter gegen dieselbe DB, sonst sind
|
|
212
|
+
* alte Rows nach dem Restart unlesbar (DEKs weg). Ohne Adapter: Klartext
|
|
213
|
+
* + Boot-Warnung. */
|
|
214
|
+
readonly kms?: KmsAdapter;
|
|
215
|
+
/** 32-Byte-Key (base64) für Blind-Index-HMACs — Pflicht sobald `kms`
|
|
216
|
+
* gesetzt ist und lookupable-Felder gemountet sind (sonst Boot-Abbruch,
|
|
217
|
+
* symmetrisch zu runProdApp). */
|
|
218
|
+
readonly blindIndexKey?: string;
|
|
179
219
|
/** Env-Quelle für die ENV→config-app-override-Brücke (Keys mit `env:`
|
|
180
220
|
* bekommen ihren env-Wert als app-override-Default — symmetrisch zu
|
|
181
221
|
* runProdApp). Default `process.env`. Injizierbar als Test-Seam, damit
|
|
@@ -226,10 +266,14 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
226
266
|
...(composeAuthOptions && { authOptions: composeAuthOptions }),
|
|
227
267
|
});
|
|
228
268
|
|
|
229
|
-
//
|
|
230
|
-
// FILE_STORAGE_PROVIDER
|
|
231
|
-
//
|
|
232
|
-
|
|
269
|
+
// An explicitly wired file provider (options.files) satisfies the
|
|
270
|
+
// FILE_STORAGE_PROVIDER boot gate — set it before validateBoot runs. Only
|
|
271
|
+
// if WE set it (532/2): deleting it after use instead of leaving a
|
|
272
|
+
// permanent process.env mutation, so a second runDevApp call in the same
|
|
273
|
+
// process (no files this time) doesn't fall through the gate falsely.
|
|
274
|
+
const setFileStorageProviderEnv =
|
|
275
|
+
options.files !== undefined && process.env["FILE_STORAGE_PROVIDER"] === undefined;
|
|
276
|
+
if (setFileStorageProviderEnv) {
|
|
233
277
|
process.env["FILE_STORAGE_PROVIDER"] = "configured";
|
|
234
278
|
}
|
|
235
279
|
|
|
@@ -238,7 +282,11 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
238
282
|
// die früher nur runProdApp fing und sonst erst den Prod-Pod im
|
|
239
283
|
// CrashLoopBackOff sterben ließ (#359). Wirft synchron, bevor ein
|
|
240
284
|
// Socket oder Watcher (codegen-Write) aufgeht.
|
|
241
|
-
|
|
285
|
+
try {
|
|
286
|
+
validateBoot(features);
|
|
287
|
+
} finally {
|
|
288
|
+
if (setFileStorageProviderEnv) delete process.env["FILE_STORAGE_PROVIDER"];
|
|
289
|
+
}
|
|
242
290
|
warnIfNonUtcServerTimeZone();
|
|
243
291
|
validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
|
|
244
292
|
|
|
@@ -303,8 +351,27 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
303
351
|
// throwaway-Registry hier extrahiert nur die config-Keys, weil der
|
|
304
352
|
// configResolver vor dem Server-Boot konstruiert wird (stack.registry
|
|
305
353
|
// gibt's erst onAfterSetup) — createKumikoServer baut intern seine eigene.
|
|
354
|
+
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
355
|
+
// App-wide cipher for `encrypted: true` entity fields (symmetrisch zu
|
|
356
|
+
// runProdApp) — executors resolve it lazily.
|
|
357
|
+
configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
|
|
358
|
+
// Subject-KMS + Blind-Index (symmetrisch zu runProdApp). Dev warnt bei
|
|
359
|
+
// Klartext-PII statt zu failen; kms+lookupable ohne blindIndexKey bricht
|
|
360
|
+
// auch hier (Lookups wären in jedem Modus kaputt).
|
|
361
|
+
configurePiiSubjectKms(options.kms);
|
|
362
|
+
configureBlindIndexKey(options.blindIndexKey);
|
|
363
|
+
assertPiiBootInvariants(features, {
|
|
364
|
+
kms: options.kms,
|
|
365
|
+
blindIndexKey: options.blindIndexKey,
|
|
366
|
+
mode: "dev",
|
|
367
|
+
});
|
|
306
368
|
const cfgExtra = effectiveAuth
|
|
307
|
-
? mergeConfigResolverDefault(
|
|
369
|
+
? mergeConfigResolverDefault(
|
|
370
|
+
options.extraContext,
|
|
371
|
+
createRegistry(features),
|
|
372
|
+
envSource,
|
|
373
|
+
bootCrypto.configCipher,
|
|
374
|
+
)
|
|
308
375
|
: options.extraContext;
|
|
309
376
|
// Auto-wire textContent (immer) + secrets (feature-gated), symmetrisch zu
|
|
310
377
|
// runProdApp. Anders als prod existiert die db hier erst im Factory-deps
|
|
@@ -319,39 +386,70 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
319
386
|
registry: deps.registry,
|
|
320
387
|
hasAuth: false,
|
|
321
388
|
sseBroker: deps.sseBroker,
|
|
322
|
-
|
|
389
|
+
crypto: bootCrypto,
|
|
323
390
|
});
|
|
324
391
|
const base = typeof cfgExtra === "function" ? cfgExtra(deps) : (cfgExtra ?? {});
|
|
325
392
|
return { ...boot, ...base };
|
|
326
393
|
};
|
|
327
394
|
|
|
328
|
-
// Sessions
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
let sessionCallbacks: SessionCallbacks | undefined;
|
|
336
|
-
const requireSessions = (): SessionCallbacks => {
|
|
337
|
-
if (!sessionCallbacks) {
|
|
338
|
-
throw new Error("[runDevApp] session-callbacks accessed before onAfterSetup");
|
|
395
|
+
// Sessions: late-bound holder — resolveSessionStore needs concrete db+
|
|
396
|
+
// registry (only after setupTestStack). Wired when sessions feature is
|
|
397
|
+
// mounted (#1372); no auth.sessions opt-in anymore.
|
|
398
|
+
let sessionStore: SessionStore | undefined;
|
|
399
|
+
const requireSessionStore = (): SessionStore => {
|
|
400
|
+
if (!sessionStore) {
|
|
401
|
+
throw new Error("[runDevApp] sessionStore accessed before onAfterSetup");
|
|
339
402
|
}
|
|
340
|
-
return
|
|
403
|
+
return sessionStore;
|
|
341
404
|
};
|
|
405
|
+
// Token-verifier opt-in: same late-bound holder pattern — resolveTokenVerifier
|
|
406
|
+
// needs the real db+registry (only concrete after setupTestStack). Wired
|
|
407
|
+
// whenever a provider feature (personal-access-tokens today) is mounted.
|
|
408
|
+
let tokenVerifier: TokenVerifier | undefined;
|
|
409
|
+
let lifecycleDb: DbConnection | undefined;
|
|
410
|
+
const patFeature = features.find((f) => f.name === PAT_FEATURE);
|
|
411
|
+
const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
|
|
412
|
+
const tenantLifecycleFeature = features.find((f) => f.name === TENANT_LIFECYCLE_FEATURE);
|
|
413
|
+
const patAuthFragment = patFeature
|
|
414
|
+
? {
|
|
415
|
+
tokenVerifier: (rawToken: string) => {
|
|
416
|
+
if (!tokenVerifier) {
|
|
417
|
+
throw new Error("[runDevApp] token-verifier accessed before onAfterSetup");
|
|
418
|
+
}
|
|
419
|
+
return tokenVerifier(rawToken);
|
|
420
|
+
},
|
|
421
|
+
patRateLimiter: (() => {
|
|
422
|
+
const rl = patRateLimitFromFeature(patFeature);
|
|
423
|
+
return createInMemoryLoginRateLimiter(rl.maxRequests, rl.windowMs);
|
|
424
|
+
})(),
|
|
425
|
+
}
|
|
426
|
+
: {};
|
|
427
|
+
|
|
428
|
+
const tenantLifecycleAuthFragment = tenantLifecycleFeature
|
|
429
|
+
? {
|
|
430
|
+
resolveTenantLifecycleStatus: async (tenantId: string) => {
|
|
431
|
+
if (!lifecycleDb) {
|
|
432
|
+
throw new Error("[runDevApp] tenant-lifecycle gate accessed before onAfterSetup");
|
|
433
|
+
}
|
|
434
|
+
const gate = await resolveTenantLifecycleGate(lifecycleDb, tenantId);
|
|
435
|
+
return gate ? { status: gate.status } : null;
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
: {};
|
|
439
|
+
|
|
440
|
+
// Parity with runProdApp (#1372 review): gate on sessionStore provider, not
|
|
441
|
+
// the sessions feature name — a custom provider must also wire auth callbacks.
|
|
442
|
+
const sessionStoreProviderMounted = features.some((f) =>
|
|
443
|
+
f.extensionUsages.some((u) => u.extensionName === EXT_SESSION_STORE),
|
|
444
|
+
);
|
|
342
445
|
const sessionAuthFragment =
|
|
343
|
-
effectiveAuth
|
|
446
|
+
effectiveAuth && sessionStoreProviderMounted
|
|
344
447
|
? {
|
|
345
448
|
sessionCreator: (user: SessionUser, meta: SessionMetadata) =>
|
|
346
|
-
|
|
347
|
-
sessionRevoker: (sid: string) =>
|
|
449
|
+
requireSessionStore().creator(user, meta),
|
|
450
|
+
sessionRevoker: (sid: string) => requireSessionStore().revoker(sid),
|
|
348
451
|
sessionChecker: (sid: string, userId: string) =>
|
|
349
|
-
|
|
350
|
-
// strict-mode: jede neue Plattform-App startet ohne legacy-
|
|
351
|
-
// JWTs ohne sid, daher safe als Default. Wer Sessions opt-in
|
|
352
|
-
// wählt, will explizite Server-side Revocation — strict-mode
|
|
353
|
-
// ist der einzige Modus der das tatsächlich erzwingt.
|
|
354
|
-
sessionStrictMode: true,
|
|
452
|
+
requireSessionStore().checker(sid, userId),
|
|
355
453
|
}
|
|
356
454
|
: {};
|
|
357
455
|
|
|
@@ -391,7 +489,17 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
391
489
|
...(effectiveAuth.unsafeSkipOriginCheck !== undefined && {
|
|
392
490
|
unsafeSkipOriginCheck: effectiveAuth.unsafeSkipOriginCheck,
|
|
393
491
|
}),
|
|
492
|
+
...(effectiveAuth.trustedProxyHops !== undefined && {
|
|
493
|
+
trustedProxyHops: effectiveAuth.trustedProxyHops,
|
|
494
|
+
}),
|
|
394
495
|
...sessionAuthFragment,
|
|
496
|
+
...patAuthFragment,
|
|
497
|
+
...tenantLifecycleAuthFragment,
|
|
498
|
+
...(mfaFeature && {
|
|
499
|
+
mfaVerifyHandler: AuthMfaHandlers.verify,
|
|
500
|
+
mfaPreauthEnableStartHandler: AuthMfaHandlers.enableStartPreauth,
|
|
501
|
+
mfaPreauthConfirmHandler: AuthMfaHandlers.enableConfirmPreauth,
|
|
502
|
+
}),
|
|
395
503
|
...(effectiveAuth.passwordReset && {
|
|
396
504
|
passwordReset: {
|
|
397
505
|
requestHandler: AuthHandlers.requestPasswordReset,
|
|
@@ -404,6 +512,12 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
404
512
|
confirmHandler: AuthHandlers.verifyEmail,
|
|
405
513
|
},
|
|
406
514
|
}),
|
|
515
|
+
...(effectiveAuth.accountUnlock && {
|
|
516
|
+
accountUnlock: {
|
|
517
|
+
requestHandler: AuthHandlers.requestAccountUnlock,
|
|
518
|
+
confirmHandler: AuthHandlers.confirmAccountUnlock,
|
|
519
|
+
},
|
|
520
|
+
}),
|
|
407
521
|
...(effectiveAuth.signup && {
|
|
408
522
|
signup: {
|
|
409
523
|
requestHandler: AuthHandlers.signupRequest,
|
|
@@ -420,6 +534,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
420
534
|
},
|
|
421
535
|
}),
|
|
422
536
|
onAfterSetup: async (stack) => {
|
|
537
|
+
lifecycleDb = stack.db;
|
|
423
538
|
// Sprint-8a: build tier-resolver BEFORE any seeds so seeds can rely
|
|
424
539
|
// on the resolver being live (e.g. seed that writes a SystemAdmin's
|
|
425
540
|
// tier-assignment can immediately read tier-cuts).
|
|
@@ -430,12 +545,25 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
430
545
|
registry: stack.registry,
|
|
431
546
|
});
|
|
432
547
|
}
|
|
433
|
-
if (effectiveAuth
|
|
434
|
-
|
|
435
|
-
|
|
548
|
+
if (effectiveAuth && stack.registry.getExtensionUsages(EXT_SESSION_STORE).length > 0) {
|
|
549
|
+
// Secure-by-default (#1372): resolve sessionStore provider; bind
|
|
550
|
+
// password-change mass-revoke + MFA revokeAllOthers.
|
|
551
|
+
const store = await resolveSessionStore({
|
|
436
552
|
db: stack.db,
|
|
437
|
-
|
|
553
|
+
registry: stack.registry,
|
|
438
554
|
});
|
|
555
|
+
const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
|
|
556
|
+
if (sessionsFeature) {
|
|
557
|
+
bindAutoRevokeFromFeature(sessionsFeature)?.(store.massRevoker);
|
|
558
|
+
}
|
|
559
|
+
if (mfaFeature) {
|
|
560
|
+
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(store.revokeAllOthers);
|
|
561
|
+
}
|
|
562
|
+
sessionStore = store;
|
|
563
|
+
}
|
|
564
|
+
if (patFeature) {
|
|
565
|
+
tokenVerifier = (rawToken) =>
|
|
566
|
+
resolveTokenVerifier({ db: stack.db, registry: stack.registry }, rawToken);
|
|
439
567
|
}
|
|
440
568
|
if (effectiveAuth) {
|
|
441
569
|
await seedAdmin(stack.db, effectiveAuth.admin);
|
|
@@ -444,7 +572,11 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
444
572
|
// Runs before user-supplied seed callbacks so those can read /
|
|
445
573
|
// override the deploy-defaults. The helper indirection is what
|
|
446
574
|
// config-seed-boot.integration.ts pins — keep it as a single call.
|
|
447
|
-
await applyBootSeeds({
|
|
575
|
+
await applyBootSeeds({
|
|
576
|
+
registry: stack.registry,
|
|
577
|
+
db: stack.db,
|
|
578
|
+
...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
|
|
579
|
+
});
|
|
448
580
|
for (const seed of options.seeds ?? []) {
|
|
449
581
|
await seed(stack);
|
|
450
582
|
}
|
|
@@ -480,10 +612,12 @@ export function mergeConfigResolverDefault(
|
|
|
480
612
|
ctx: CreateKumikoServerOptions["extraContext"],
|
|
481
613
|
registry: Registry,
|
|
482
614
|
envSource: Record<string, string | undefined>,
|
|
615
|
+
cipher?: EnvelopeCipher,
|
|
483
616
|
): CreateKumikoServerOptions["extraContext"] {
|
|
484
617
|
const defaults = {
|
|
485
618
|
configResolver: createConfigResolver({
|
|
486
619
|
appOverrides: buildEnvConfigOverrides(registry, envSource),
|
|
620
|
+
...(cipher && { cipher }),
|
|
487
621
|
}),
|
|
488
622
|
};
|
|
489
623
|
// ctx.config wird per-Request aus _configAccessorFactory geminted (siehe
|
|
@@ -47,14 +47,27 @@ export function scaffoldAppFeature(options: ScaffoldAppFeatureOptions): Scaffold
|
|
|
47
47
|
}
|
|
48
48
|
mkdirSync(featureDir, { recursive: true });
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
// Volle App-Feature-Konvention (Referenz: bundled-features/tenant):
|
|
51
|
+
// feature.ts = nur Registrierung, schema/ für Entities, i18n.ts für
|
|
52
|
+
// Server-Keys, web/ für die Client-Seite, __tests__/ mit Boot-Test.
|
|
53
|
+
// handlers/ + lib/ entstehen beim ersten echten Handler/Logik-Modul.
|
|
54
|
+
mkdirSync(join(featureDir, "schema"), { recursive: true });
|
|
55
|
+
mkdirSync(join(featureDir, "web"), { recursive: true });
|
|
56
|
+
mkdirSync(join(featureDir, "__tests__"), { recursive: true });
|
|
54
57
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const files: string[] = [];
|
|
59
|
+
const write = (relative: string, content: string): void => {
|
|
60
|
+
writeFileSync(join(featureDir, relative), content);
|
|
61
|
+
files.push(`src/features/${options.name}/${relative}`);
|
|
62
|
+
};
|
|
63
|
+
write("feature.ts", renderFeature(options.name));
|
|
64
|
+
write("index.ts", renderIndex(options.name));
|
|
65
|
+
write("constants.ts", renderConstants(options.name));
|
|
66
|
+
write("i18n.ts", renderI18n(options.name));
|
|
67
|
+
write(join("schema", `${options.name}-item.ts`), renderEntity(options.name));
|
|
68
|
+
write(join("schema", "index.ts"), renderSchemaIndex(options.name));
|
|
69
|
+
write(join("web", "index.ts"), renderWebIndex(options.name));
|
|
70
|
+
write(join("__tests__", "feature.boot.test.ts"), renderBootTest(options.name));
|
|
58
71
|
|
|
59
72
|
const runConfigPath = join(appRoot, "src", "run-config.ts");
|
|
60
73
|
let autoMounted = false;
|
|
@@ -83,18 +96,38 @@ function renderFeature(name: string): string {
|
|
|
83
96
|
const camel = kebabToCamel(name);
|
|
84
97
|
return `// ${name} feature — scaffolded by \`kumiko add feature\`. Edit freely.
|
|
85
98
|
//
|
|
86
|
-
//
|
|
87
|
-
// (r.entity
|
|
99
|
+
// Konvention (Referenz: bundled-features, z.B. tenant/):
|
|
100
|
+
// feature.ts — NUR Registrierung (r.entity / r.screen / r.queryHandler / …)
|
|
101
|
+
// schema/ — Entity-Definitionen
|
|
102
|
+
// handlers/ — eine Datei pro Handler (defineQueryHandler / defineWriteHandler)
|
|
103
|
+
// lib/ — reine Domain-Logik
|
|
104
|
+
// web/ — Client-Seite (index.ts, ein Screen pro Datei)
|
|
105
|
+
// i18n.ts — Server-Translation-Keys
|
|
106
|
+
//
|
|
107
|
+
// Doc-Pointer: https://docs.kumiko.rocks/en/patterns/ for the \`r.*\` API.
|
|
88
108
|
|
|
89
109
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
110
|
+
import { ${constName(name)} } from "./constants";
|
|
111
|
+
import { ${camel}TranslationKeys } from "./i18n";
|
|
112
|
+
import { ${camel}ItemEntity } from "./schema";
|
|
113
|
+
|
|
114
|
+
export const ${camel}Feature = defineFeature(${constName(name)}, (r) => {
|
|
115
|
+
r.entity("${name}-item", ${camel}ItemEntity);
|
|
90
116
|
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
// Deklarativer List-Screen — CRUD-Query kommt aus der Entity.
|
|
118
|
+
// Für query-getriebene Tabellen: type "projectionList"; für
|
|
119
|
+
// Kennzahlen-Grids: type "dashboard".
|
|
120
|
+
r.screen({
|
|
121
|
+
id: "items",
|
|
122
|
+
type: "entityList",
|
|
123
|
+
entity: "${name}-item",
|
|
124
|
+
columns: ["title"],
|
|
125
|
+
defaultSort: { field: "title", dir: "asc" },
|
|
97
126
|
});
|
|
127
|
+
|
|
128
|
+
r.translations({ keys: ${camel}TranslationKeys });
|
|
129
|
+
|
|
130
|
+
r.nav({ id: "items", label: "screen:items.title", screen: "${name}:screen:items" });
|
|
98
131
|
});
|
|
99
132
|
`;
|
|
100
133
|
}
|
|
@@ -104,10 +137,80 @@ function renderIndex(name: string): string {
|
|
|
104
137
|
return `export { ${camel}Feature } from "./feature";\n`;
|
|
105
138
|
}
|
|
106
139
|
|
|
140
|
+
function renderConstants(name: string): string {
|
|
141
|
+
return `export const ${constName(name)} = "${name}" as const;\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderI18n(name: string): string {
|
|
145
|
+
const camel = kebabToCamel(name);
|
|
146
|
+
return `// Server-seitige Translation-Keys (r.translations). Screen-Titel und
|
|
147
|
+
// Field-Labels folgen den Framework-Konventionen — der Boot-Validator
|
|
148
|
+
// prüft die Abdeckung.
|
|
149
|
+
|
|
150
|
+
export const ${camel}TranslationKeys = {
|
|
151
|
+
"screen:items.title": { de: "Einträge", en: "Items" },
|
|
152
|
+
"${name}:entity:${name}-item:field:title": { de: "Titel", en: "Title" },
|
|
153
|
+
};
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderEntity(name: string): string {
|
|
158
|
+
const camel = kebabToCamel(name);
|
|
159
|
+
return `// Entity-Definition — Felder, Annotationen, Transitions.
|
|
160
|
+
// Starter: eine Text-Spalte. Ersetze sie durch deine Domain.
|
|
161
|
+
import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
|
|
162
|
+
|
|
163
|
+
export const ${camel}ItemEntity = createEntity({
|
|
164
|
+
fields: {
|
|
165
|
+
title: createTextField({ required: true, sortable: true }),
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderSchemaIndex(name: string): string {
|
|
172
|
+
const camel = kebabToCamel(name);
|
|
173
|
+
return `export { ${camel}ItemEntity } from "./${name}-item";\n`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderWebIndex(name: string): string {
|
|
177
|
+
const camel = kebabToCamel(name);
|
|
178
|
+
return `// @runtime client
|
|
179
|
+
// Client-Seite des Features: translations, components (custom-Screens),
|
|
180
|
+
// columnRenderers. In src/app/client.tsx bei
|
|
181
|
+
// createKumikoApp({ clientFeatures: [...] }) einhängen — das passiert
|
|
182
|
+
// NICHT automatisch. Screens: eine Datei pro Screen in diesem Ordner.
|
|
183
|
+
|
|
184
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
185
|
+
|
|
186
|
+
export const ${camel}Client: ClientFeatureDefinition = {
|
|
187
|
+
name: "${name}",
|
|
188
|
+
};
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function renderBootTest(name: string): string {
|
|
193
|
+
const camel = kebabToCamel(name);
|
|
194
|
+
return `import { describe, expect, test } from "bun:test";
|
|
195
|
+
import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
196
|
+
import { ${camel}Feature } from "../feature";
|
|
197
|
+
|
|
198
|
+
describe("${name} feature", () => {
|
|
199
|
+
test("boots: Entity, Screens und i18n-Coverage validieren", () => {
|
|
200
|
+
expect(() => validateBoot([${camel}Feature])).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
107
206
|
function kebabToCamel(name: string): string {
|
|
108
207
|
return name.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
|
109
208
|
}
|
|
110
209
|
|
|
210
|
+
function constName(name: string): string {
|
|
211
|
+
return `${name.replace(/-/g, "_").toUpperCase()}_FEATURE`;
|
|
212
|
+
}
|
|
213
|
+
|
|
111
214
|
// ts-morph: open run-config, prepend import, append APP_FEATURES entry.
|
|
112
215
|
// Returns true on success, throws on shape-mismatch — the caller rolls back
|
|
113
216
|
// the scaffolded feature dir and re-throws so a re-run isn't blocked.
|