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