@cosmicdrift/kumiko-dev-server 0.386.0 → 0.388.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 (56) hide show
  1. package/LICENSE +21 -57
  2. package/dist/build-server-bundle.d.ts +7 -7
  3. package/dist/build-server-bundle.js +53 -53
  4. package/dist/build.js +3 -3
  5. package/dist/compose-stacks.d.ts +2 -2
  6. package/dist/compose-stacks.js +1 -2
  7. package/dist/crash-tracker.d.ts +3 -7
  8. package/dist/crash-tracker.js +4 -13
  9. package/dist/create-kumiko-server-client-bundles.d.ts +21 -0
  10. package/dist/create-kumiko-server-client-bundles.js +97 -0
  11. package/dist/create-kumiko-server-html.d.ts +21 -0
  12. package/dist/create-kumiko-server-html.js +132 -0
  13. package/dist/create-kumiko-server-public-files.d.ts +6 -0
  14. package/dist/create-kumiko-server-public-files.js +96 -0
  15. package/dist/create-kumiko-server-reload.d.ts +14 -0
  16. package/dist/create-kumiko-server-reload.js +73 -0
  17. package/dist/create-kumiko-server-routes.d.ts +16 -0
  18. package/dist/create-kumiko-server-routes.js +206 -0
  19. package/dist/create-kumiko-server-shutdown.d.ts +14 -0
  20. package/dist/create-kumiko-server-shutdown.js +72 -0
  21. package/dist/create-kumiko-server-stack.d.ts +12 -0
  22. package/dist/create-kumiko-server-stack.js +101 -0
  23. package/dist/create-kumiko-server-stylesheet.d.ts +10 -0
  24. package/dist/create-kumiko-server-stylesheet.js +137 -0
  25. package/dist/create-kumiko-server-watcher.d.ts +22 -0
  26. package/dist/create-kumiko-server-watcher.js +155 -0
  27. package/dist/create-kumiko-server.d.ts +53 -71
  28. package/dist/create-kumiko-server.js +59 -1076
  29. package/dist/dev-server-runtime.d.ts +5 -0
  30. package/dist/dev-server-runtime.js +11 -0
  31. package/dist/env-schema.d.ts +1 -1
  32. package/dist/env-schema.js +4 -4
  33. package/dist/index.d.ts +3 -1
  34. package/dist/index.js +17 -15
  35. package/dist/managed-files/app-jobs.js +1 -1
  36. package/dist/managed-files/check.js +1 -1
  37. package/dist/run-dev-app-auth.d.ts +38 -0
  38. package/dist/run-dev-app-auth.js +155 -0
  39. package/dist/run-dev-app-config.d.ts +26 -0
  40. package/dist/run-dev-app-config.js +185 -0
  41. package/dist/run-dev-app-server-options.d.ts +13 -0
  42. package/dist/run-dev-app-server-options.js +49 -0
  43. package/dist/run-dev-app.d.ts +52 -50
  44. package/dist/run-dev-app.js +100 -471
  45. package/dist/scaffold-app-feature.js +5 -4
  46. package/dist/scaffold-app.js +5 -7
  47. package/dist/schema-apply.d.ts +3 -3
  48. package/dist/schema-apply.js +16 -16
  49. package/dist/schema-tables-auth-mode.js +16 -15
  50. package/dist/schema-tables-minimal.js +7 -7
  51. package/dist/setup-test-stack-from-features.js +1 -1
  52. package/dist/welcome-banner.js +2 -2
  53. package/dist/write-server-lockfile.js +23 -16
  54. package/package.json +10 -10
  55. package/src/changes.json +30 -0
  56. package/templates/managed/ci-public.yml.template +4 -0
@@ -0,0 +1,5 @@
1
+ /// <reference types="bun-types" preserve="true" />
2
+ export declare const hasBun: boolean;
3
+ export type BunServer = typeof Bun extends undefined ? unknown : ReturnType<typeof Bun.serve>;
4
+ export declare const logInfo: (msg: string) => void;
5
+ export declare const logError: (...args: unknown[]) => void;
@@ -0,0 +1,11 @@
1
+ /// <reference types="bun-types" preserve="true" />
2
+ // The dev server is meant to run under Bun, but this module must at least
3
+ // load without it: every Bun.* call is gated, and tests drive the fetch
4
+ // handler directly instead of going through Bun.serve and real sockets.
5
+ export const hasBun = typeof Bun !== "undefined";
6
+ // @wrapper-known semantic-alias
7
+ // biome-ignore lint/suspicious/noConsole: dev-server status logging
8
+ export const logInfo = (msg) => console.log(msg);
9
+ // @wrapper-known semantic-alias
10
+ // biome-ignore lint/suspicious/noConsole: dev-server error logging
11
+ export const logError = (...args) => console.error(...args);
@@ -4,7 +4,7 @@ import * as z from "zod";
4
4
  *
5
5
  * PORT-default "3000" is the same fallback as
6
6
  * `packages/dev-server/src/run-prod-app.ts:533` — keep in sync when the
7
- * call-site is refactored to consume parsed env (Sprint 9.5 Phase 4). */
7
+ * call-site is refactored to consume parsed env. */
8
8
  export declare const frameworkCoreEnvSchema: z.ZodObject<{
9
9
  PORT: z.ZodDefault<z.ZodString>;
10
10
  DATABASE_URL: z.ZodURL;
@@ -1,26 +1,26 @@
1
1
  // @runtime runtime
2
2
  // Consumed at prod boot via composeEnvSchema (same as compose-stacks),
3
- // not only by tooling — keep runtime-isolation-safe (kumiko-framework#2340).
3
+ // not only by tooling — keep runtime-isolation-safe.
4
4
  //
5
5
  // Framework-core env-schema — the vars that runProdApp + buildServer
6
6
  // + the connection-pool read directly from process.env today. Apps merge
7
7
  // this into their app-wide schema via `composeEnvSchema({ core, ... })`.
8
8
  //
9
- // Sprint 9.2 is purely additive: the schema is exposed, but the call-sites
9
+ // The schema is purely additive: it is exposed, but the call-sites
10
10
  // in run-prod-app.ts (requireEnv("DATABASE_URL"), …) and api/server.ts
11
11
  // (process.env["KUMIKO_INSTANCE_ID"]) keep reading process.env directly.
12
12
  // Apps that opt into the schema get aggregated boot-validation errors
13
13
  // BEFORE those legacy reads run; apps that don't, behave as before.
14
14
  //
15
15
  // Feature-specific vars (JWT_SECRET, KUMIKO_SECRETS_MASTER_KEY_*) live
16
- // in their owning feature's envSchema — Phase 2 (bundled-features).
16
+ // in their owning feature's envSchema (bundled-features).
17
17
  import * as z from "zod";
18
18
  /** Env-vars read by framework-core (api/server, db/connection,
19
19
  * dev-server/run-prod-app). NOT including feature-specific vars.
20
20
  *
21
21
  * PORT-default "3000" is the same fallback as
22
22
  * `packages/dev-server/src/run-prod-app.ts:533` — keep in sync when the
23
- * call-site is refactored to consume parsed env (Sprint 9.5 Phase 4). */
23
+ * call-site is refactored to consume parsed env. */
24
24
  export const frameworkCoreEnvSchema = z.object({
25
25
  PORT: z
26
26
  .string()
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { type ComposeFeaturesOptions, composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
2
2
  export { type AuthStackOptions, composeAuthStack, composeFileStack, composeGdprStack, composeIdentityStack, composeMailStack, composeOpsStack, composePagesStack, composeRendererStack, composeSaasStack, composeSecurityStack, composeUserDataRightsStack, type FileProviderKind, type FileStackOptions, type GdprStackOptions, type GdprStackOrder, type IdentityStackOptions, type MailStackOptions, type MailTransportKind, type OpsStackOptions, type PagesStackOptions, type SaasStackBilling, type SaasStackOptions, type SecurityStackOptions, stackFeatureNames, type UserDataRightsStackOptions, } from "./compose-stacks.ts";
3
- export { type CreateKumikoServerOptions, createKumikoServer, type DevHostDispatch, type DevHostDispatchResult, type KumikoServerHandle, PROD_BUNDLES_ENV, resolveStylesheet, STYLESHEET_WATCH_ENV, } from "./create-kumiko-server.ts";
3
+ export { type CreateKumikoServerOptions, createKumikoServer, type DevHostDispatch, type DevHostDispatchResult, type KumikoServerHandle, } from "./create-kumiko-server.ts";
4
+ export { PROD_BUNDLES_ENV } from "./create-kumiko-server-client-bundles.ts";
5
+ export { resolveStylesheet, STYLESHEET_WATCH_ENV } from "./create-kumiko-server-stylesheet.ts";
4
6
  export { type FrameworkCoreEnv, frameworkCoreEnvSchema } from "./env-schema.ts";
5
7
  export type { AuthoringStyle, BuildFewShotCorpusOptions, CorpusWarning, FewShotCorpus, FewShotEntry, } from "./few-shot-corpus.ts";
6
8
  export { buildFewShotCorpus, pathToId } from "./few-shot-corpus.ts";
package/dist/index.js CHANGED
@@ -1,24 +1,26 @@
1
- // Public API für den Kumiko-Dev-Server. Zwei Schichten:
1
+ // Public API for the Kumiko development server, with two layers:
2
2
  //
3
3
  // - createKumikoServer (low-level)
4
- // Bun.serve-Wrapper der Client-Bundle, /styles.css, AppSchema-
5
- // Injection, SSE-Reload und einen Auto-Mint-JWT-Modus liefert. Nimmt
6
- // features + clientEntry direkt an, kein Auth-Auto-Wiring. Wer
7
- // einen ungewöhnlichen Auth-Setup braucht (alternative Membership-
8
- // Query, eigener Rate-Limiter, custom Login-Routes) geht hier rein.
4
+ // Bun.serve wrapper that provides the client bundle, /styles.css,
5
+ // AppSchema injection, SSE reload, and auto-minted JWTs. Accepts
6
+ // features + clientEntry directly without automatic auth wiring. Use it
7
+ // for unusual auth setups (alternative Membership query, custom rate
8
+ // limiter, or custom login routes).
9
9
  //
10
10
  // - runDevApp (high-level)
11
- // Mischt die Standard-Features (config/user/tenant/auth-email-
12
- // password) automatisch dazu wenn `auth` gesetzt ist, wired die
13
- // Login-Routes + Error-Map, ruft seedAdminGuarded im onAfterSetup. Default
14
- // für Sample-Apps und Showcases — 5-10 Zeilen Bootstrap statt 50.
11
+ // Automatically adds the standard features (config/user/tenant/
12
+ // auth-email-password) when `auth` is set, wires the login routes and
13
+ // error map, and calls seedAdminGuarded in onAfterSetup. The default for
14
+ // sample apps and showcases: 5–10 lines of bootstrap instead of 50.
15
15
  export { composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
16
- // Build-Toolchain (buildProdBundle, Bun.build, Tailwind-Pipeline, ts-morph) lebt
17
- // im Sub-Path-Export `@cosmicdrift/kumiko-dev-server/build`. Damit zieht der Main-Barrel
18
- // kein Bun-Toolchain-Bundle mehr in Production-Reads (z.B. wenn drizzle-kit
19
- // die App-Config unter Node lädt).
16
+ // The build toolchain (buildProdBundle, Bun.build, Tailwind pipeline, ts-morph)
17
+ // lives in the subpath export `@cosmicdrift/kumiko-dev-server/build`. This keeps
18
+ // the Bun toolchain bundle out of production imports from the main barrel (for
19
+ // example, when drizzle-kit loads the app config under Node).
20
20
  export { composeAuthStack, composeFileStack, composeGdprStack, composeIdentityStack, composeMailStack, composeOpsStack, composePagesStack, composeRendererStack, composeSaasStack, composeSecurityStack, composeUserDataRightsStack, stackFeatureNames, } from "./compose-stacks.js";
21
- export { createKumikoServer, PROD_BUNDLES_ENV, resolveStylesheet, STYLESHEET_WATCH_ENV, } from "./create-kumiko-server.js";
21
+ export { createKumikoServer, } from "./create-kumiko-server.js";
22
+ export { PROD_BUNDLES_ENV } from "./create-kumiko-server-client-bundles.js";
23
+ export { resolveStylesheet, STYLESHEET_WATCH_ENV } from "./create-kumiko-server-stylesheet.js";
22
24
  export { frameworkCoreEnvSchema } from "./env-schema.js";
23
25
  export { buildFewShotCorpus, pathToId } from "./few-shot-corpus.js";
24
26
  export { resolveFrameworkVersion } from "./framework-version.js";
@@ -1,7 +1,7 @@
1
1
  // Jobs named `app-<name>` in a managed workflow belong to the app: drift
2
2
  // checks ignore them and a sync carries them over.
3
3
  const JOBS_HEADER = /^jobs:\s*$/;
4
- const JOB_START = /^ {2}([A-Za-z0-9_-]+):\s*$/;
4
+ const JOB_START = /^ {2}["']?([A-Za-z0-9_-]+)["']?:\s*(?:#.*)?$/;
5
5
  const TOP_LEVEL_KEY = /^[^\s#]/;
6
6
  const APP_JOB_PREFIX = "app-";
7
7
  function isDetachedComment(line) {
@@ -53,7 +53,7 @@ function checkStructural(file, facts, existing) {
53
53
  case "legacy":
54
54
  return {
55
55
  state: "invalid",
56
- message: "default-export defineAppConfig({ features, client, ports }) from @cosmicdrift/kumiko-framework/config",
56
+ message: "default-export defineAppConfig({ features: [...], client: { entries: [...] }, ports: { dev: 4173, e2e: 4174 } }) from @cosmicdrift/kumiko-framework/config",
57
57
  };
58
58
  case "invalid":
59
59
  return { state: "invalid", message: facts.config.message };
@@ -0,0 +1,38 @@
1
+ import { type SessionStore } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
+ import type { SessionMetadata, TokenVerifier } from "@cosmicdrift/kumiko-framework/api";
3
+ import type { EffectiveFeaturesResolver, FeatureDefinition, Registry, SessionUser } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
5
+ import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";
6
+ import type { DevBootConfig } from "./run-dev-app-config.ts";
7
+ export type DevLateBindings = {
8
+ tierResolver: EffectiveFeaturesResolver | undefined;
9
+ sessionStore: SessionStore | undefined;
10
+ tokenVerifier: TokenVerifier | undefined;
11
+ readonly resets: Array<() => void>;
12
+ };
13
+ export declare function createDevLateBindings(): DevLateBindings;
14
+ export declare function createLateBoundEffectiveFeatures(lateBindings: DevLateBindings, enabled: boolean): EffectiveFeaturesResolver | undefined;
15
+ declare function createDevSessionAuthFragment(lateBindings: DevLateBindings): {
16
+ sessionCreator: (user: SessionUser, meta: SessionMetadata) => Promise<string>;
17
+ sessionRevoker: (sid: string) => Promise<void>;
18
+ sessionChecker: (sid: string, userId: string) => Promise<import("@cosmicdrift/kumiko-framework/api").AuthSessionCheckResult>;
19
+ };
20
+ declare function createDevPatAuthFragment(patFeature: FeatureDefinition, lateBindings: DevLateBindings): {
21
+ tokenVerifier: (rawToken: string) => Promise<SessionUser | null>;
22
+ patRateLimiter: import("@cosmicdrift/kumiko-framework/api").LoginRateLimiter;
23
+ };
24
+ export type DevAuthWiring = {
25
+ readonly patFeature: FeatureDefinition | undefined;
26
+ readonly mfaFeature: FeatureDefinition | undefined;
27
+ readonly sessionAuthFragment: ReturnType<typeof createDevSessionAuthFragment> | undefined;
28
+ readonly patAuthFragment: ReturnType<typeof createDevPatAuthFragment> | undefined;
29
+ };
30
+ export declare function wireDevAuth(config: Pick<DevBootConfig, "features" | "effectiveAuth" | "envSource">, registry: Registry | undefined, sessionStoreProviderMounted: boolean, lateBindings: DevLateBindings): DevAuthWiring;
31
+ export type DevAfterSetupContext = {
32
+ readonly config: DevBootConfig;
33
+ readonly bootCrypto: ReturnType<typeof resolveBootCrypto>;
34
+ readonly auth: DevAuthWiring;
35
+ readonly lateBindings: DevLateBindings;
36
+ };
37
+ export declare function runDevAfterSetup(stack: TestStack, ctx: DevAfterSetupContext): Promise<void>;
38
+ export {};
@@ -0,0 +1,155 @@
1
+ import { EXT_SESSION_STORE, resolveSessionStore, resolveTokenVerifier, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
+ import { AUTH_MFA_FEATURE, bindMfaRevokeAllOtherSessionsFromFeature, bindMfaTokenSecretsFromFeature, bindRevokeAllPatTokensFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
3
+ import { bindPatAutoRevokeOnPasswordChangeFromFeature, PAT_FEATURE, patRateLimitFromFeature, revokeAllPatTokensForUser, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
4
+ import { bindAutoRevokeFromFeature, SESSIONS_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/sessions";
5
+ import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
6
+ import { applyStandardBootSeeds, assertMfaSwitchTenantGate, buildEffectiveFeaturesResolver, } from "@cosmicdrift/kumiko-server-runtime/boot/shared-boot-steps";
7
+ export function createDevLateBindings() {
8
+ return { tierResolver: undefined, sessionStore: undefined, tokenVerifier: undefined, resets: [] };
9
+ }
10
+ // Tier-Composition auto-wire: scan features for a
11
+ // tenantTierResolver-extension and wire a late-bound wrapper here; it is
12
+ // filled in onAfterSetup (where stack.db + stack.registry are available).
13
+ // The global operator toggles layer over the same wrapper.
14
+ // App-Author sees nothing — `createTierEngineFeature(opts)` mounts +
15
+ // framework auto-wires.
16
+ export function createLateBoundEffectiveFeatures(lateBindings, enabled) {
17
+ if (!enabled)
18
+ return undefined;
19
+ return Object.assign((tenantId) => {
20
+ // Defensive: Server starts AFTER onAfterSetup completes, so the
21
+ // resolver is filled before any request comes in. Throwing here
22
+ // means a programming error (boot order) rather than silent
23
+ // "all-features-on" misbehavior.
24
+ if (!lateBindings.tierResolver) {
25
+ throw new Error("tier-resolver: extension found but resolver not yet built — boot order issue?");
26
+ }
27
+ return lateBindings.tierResolver(tenantId);
28
+ }, {
29
+ // The real trialGate lands on the holder's resolver only after plugin.build(). Delegate rather than
30
+ // copy it because this wrapper closure is created before build().
31
+ // Return false when no trial gate is available.
32
+ trialGate: (tenantId, featureName) => lateBindings.tierResolver?.trialGate?.(tenantId, featureName) ?? Promise.resolve(false),
33
+ });
34
+ }
35
+ // Sessions: late-bound holder — resolveSessionStore needs concrete db+
36
+ // registry (only after setupTestStack). Wired when sessions feature is
37
+ // mounted; no auth.sessions opt-in anymore.
38
+ function createDevSessionAuthFragment(lateBindings) {
39
+ const requireSessionStore = () => {
40
+ if (!lateBindings.sessionStore) {
41
+ throw new Error("[runDevApp] sessionStore accessed before onAfterSetup");
42
+ }
43
+ return lateBindings.sessionStore;
44
+ };
45
+ return {
46
+ sessionCreator: (user, meta) => requireSessionStore().creator(user, meta),
47
+ sessionRevoker: (sid) => requireSessionStore().revoker(sid),
48
+ sessionChecker: (sid, userId) => requireSessionStore().checker(sid, userId),
49
+ };
50
+ }
51
+ // Token-verifier opt-in: same late-bound holder pattern — resolveTokenVerifier
52
+ // needs the real db+registry (only concrete after setupTestStack). Wired
53
+ // whenever a provider feature (personal-access-tokens today) is mounted.
54
+ function createDevPatAuthFragment(patFeature, lateBindings) {
55
+ const rl = patRateLimitFromFeature(patFeature);
56
+ return {
57
+ tokenVerifier: (rawToken) => {
58
+ if (!lateBindings.tokenVerifier) {
59
+ throw new Error("[runDevApp] token-verifier accessed before onAfterSetup");
60
+ }
61
+ return lateBindings.tokenVerifier(rawToken);
62
+ },
63
+ patRateLimiter: createInMemoryLoginRateLimiter(rl.maxRequests, rl.windowMs),
64
+ };
65
+ }
66
+ export function wireDevAuth(config, registry, sessionStoreProviderMounted, lateBindings) {
67
+ const { features, effectiveAuth, envSource } = config;
68
+ const patFeature = features.find((f) => f.name === PAT_FEATURE);
69
+ const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
70
+ // Same master secret as runProdApp. Without JWT_SECRET the feature keeps
71
+ // its explicit secrets, or its handlers report the missing binding.
72
+ const devMasterSecret = envSource["JWT_SECRET"];
73
+ if (mfaFeature && devMasterSecret) {
74
+ bindMfaTokenSecretsFromFeature(mfaFeature)?.(devMasterSecret, envSource);
75
+ }
76
+ if (effectiveAuth && mfaFeature && registry) {
77
+ assertMfaSwitchTenantGate(registry, "runDevApp");
78
+ }
79
+ const patAuthFragment = patFeature
80
+ ? createDevPatAuthFragment(patFeature, lateBindings)
81
+ : undefined;
82
+ // Parity with runProdApp: gate on sessionStore provider, not
83
+ // the sessions feature name — a custom provider must also wire auth
84
+ // callbacks. sessionStoreProviderMounted computed via the registry
85
+ // (same check as runProdApp) — not recomputed here.
86
+ const sessionAuthFragment = effectiveAuth && sessionStoreProviderMounted
87
+ ? createDevSessionAuthFragment(lateBindings)
88
+ : undefined;
89
+ return { patFeature, mfaFeature, sessionAuthFragment, patAuthFragment };
90
+ }
91
+ // Binds a late-bound revoker and records its unbind for when the app stops.
92
+ function bindReleasable(bind, revoker, resets) {
93
+ if (bind)
94
+ resets.push(bind(revoker));
95
+ }
96
+ async function bindDevSessionStore(stack, ctx) {
97
+ const { config, auth, lateBindings } = ctx;
98
+ const { patFeature, mfaFeature } = auth;
99
+ // Secure-by-default: resolve sessionStore provider; bind
100
+ // password-change mass-revoke + MFA revokeAllOthers.
101
+ const store = await resolveSessionStore({
102
+ db: stack.db,
103
+ registry: stack.registry,
104
+ });
105
+ const sessionsFeature = config.features.find((f) => f.name === SESSIONS_FEATURE);
106
+ if (sessionsFeature) {
107
+ bindReleasable(bindAutoRevokeFromFeature(sessionsFeature), store.massRevoker, lateBindings.resets);
108
+ }
109
+ if (mfaFeature) {
110
+ bindReleasable(bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature), store.revokeAllOthers, lateBindings.resets);
111
+ }
112
+ if (mfaFeature && patFeature) {
113
+ bindReleasable(bindRevokeAllPatTokensFromFeature(mfaFeature), (userId) => revokeAllPatTokensForUser(stack.db, userId), lateBindings.resets);
114
+ }
115
+ lateBindings.sessionStore = store;
116
+ }
117
+ function bindDevPatVerifier(stack, patFeature, lateBindings) {
118
+ lateBindings.tokenVerifier = (rawToken) => resolveTokenVerifier({ db: stack.db, registry: stack.registry }, rawToken);
119
+ // Password-change → PAT-revoke: independent of sessions/MFA being
120
+ // mounted, so wired unconditionally here whenever PAT itself is.
121
+ bindReleasable(bindPatAutoRevokeOnPasswordChangeFromFeature(patFeature), (userId) => revokeAllPatTokensForUser(stack.db, userId), lateBindings.resets);
122
+ }
123
+ export async function runDevAfterSetup(stack, ctx) {
124
+ const { config, bootCrypto, auth, lateBindings } = ctx;
125
+ const { wiring, effectiveAuth, features } = config;
126
+ // Build tier-resolver BEFORE any seeds so seeds can rely
127
+ // on the resolver being live (e.g. seed that writes a SystemAdmin's
128
+ // tier-assignment can immediately read tier-cuts).
129
+ lateBindings.tierResolver = await buildEffectiveFeaturesResolver({
130
+ features,
131
+ featureToggles: wiring.featureToggles,
132
+ db: stack.db,
133
+ registry: stack.registry,
134
+ cacheSync: stack.cacheSync,
135
+ });
136
+ if (effectiveAuth && stack.registry.getExtensionUsages(EXT_SESSION_STORE).length > 0) {
137
+ await bindDevSessionStore(stack, ctx);
138
+ }
139
+ if (auth.patFeature) {
140
+ bindDevPatVerifier(stack, auth.patFeature, lateBindings);
141
+ }
142
+ // Apply r.config({ seeds }) declared by any registered feature.
143
+ // Runs before user-supplied seed callbacks so those can read /
144
+ // override the deploy-defaults.
145
+ await applyStandardBootSeeds({
146
+ db: stack.db,
147
+ registry: stack.registry,
148
+ admin: effectiveAuth?.admin,
149
+ shortcutSeeds: wiring.shortcutSeeds,
150
+ cipher: bootCrypto.configCipher,
151
+ });
152
+ for (const seed of config.stackSeeds) {
153
+ await seed(stack);
154
+ }
155
+ }
@@ -0,0 +1,26 @@
1
+ import { type FeatureDefinition, type Registry } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type BootOptionWiring } from "@cosmicdrift/kumiko-server-runtime";
3
+ import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
4
+ import { resolveAuthMail, resolveUnsubscribeWiring } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
5
+ import type { RunDevAppAuthOptions, RunDevAppOptions, SeedFn } from "./run-dev-app.ts";
6
+ export type BootFailureCleanup = {
7
+ release: (() => Promise<void>) | undefined;
8
+ };
9
+ export type DevBootConfig = {
10
+ readonly stackSeeds: readonly SeedFn[];
11
+ readonly wiring: BootOptionWiring;
12
+ readonly envSource: Record<string, string | undefined>;
13
+ readonly effectiveAuth: ReturnType<typeof resolveAuthMail<RunDevAppAuthOptions>> | undefined;
14
+ readonly unsubscribeLinks: ReturnType<typeof resolveUnsubscribeWiring>["unsubscribeLinks"];
15
+ readonly extraRoutes: RunDevAppOptions["extraRoutes"];
16
+ readonly features: readonly FeatureDefinition[];
17
+ };
18
+ export declare function resolveDevBootConfig(options: RunDevAppOptions, bootFailure: BootFailureCleanup): Promise<DevBootConfig>;
19
+ export declare function validateDevBoot(options: RunDevAppOptions, config: DevBootConfig): ReturnType<typeof resolveBootCrypto>;
20
+ export declare function configureDevBootCrypto(options: RunDevAppOptions, config: DevBootConfig, bootCrypto: ReturnType<typeof resolveBootCrypto>, resetLateBindings: ReadonlyArray<() => void>, bootFailure: BootFailureCleanup): {
21
+ readonly releasePiiKms: () => Promise<void>;
22
+ };
23
+ export declare function resolveDevAuthRegistry(config: DevBootConfig): {
24
+ readonly registry: Registry | undefined;
25
+ readonly sessionStoreProviderMounted: boolean;
26
+ };
@@ -0,0 +1,185 @@
1
+ import { EXT_SESSION_STORE } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
+ import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
3
+ import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
4
+ import { createRegistry, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { parseEnv } from "@cosmicdrift/kumiko-framework/env";
6
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
7
+ import { assertStackBootRequirements, isSeedFnList, rejectRemovedEffectiveFeatures, resolveBootOptionWiring, withBootOptionEnvBlocks, } from "@cosmicdrift/kumiko-server-runtime";
8
+ import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
9
+ import { composeAppFeatures, validateComposedFeatures, withEnvDefaults, } from "@cosmicdrift/kumiko-server-runtime/boot/shared-boot-steps";
10
+ import { assertPiiBootInvariants } from "@cosmicdrift/kumiko-server-runtime/pii-boot-gate";
11
+ import { requireEnv, resolveAuthMail, resolveUnsubscribeWiring, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
12
+ import { assertSessionBootInvariants } from "@cosmicdrift/kumiko-server-runtime/session-boot-gate";
13
+ import { logInfo } from "./dev-server-runtime.js";
14
+ import { captureEphemeralDevPiiKms, resolveDevPiiKms, } from "./resolve-dev-pii-kms.js";
15
+ function splitDevSeeds(options) {
16
+ const seedsConfig = options.seeds !== undefined && !isSeedFnList(options.seeds) ? options.seeds : undefined;
17
+ const stackSeeds = options.seeds === undefined
18
+ ? []
19
+ : isSeedFnList(options.seeds)
20
+ ? options.seeds
21
+ : (options.seeds.run ?? []);
22
+ return { seedsConfig, stackSeeds };
23
+ }
24
+ function parseDevEnvSchema(options, seedsConfig, rawEnvSource, envSchema) {
25
+ // Free dev seeds take the test stack; the env blocks only read the declarative part.
26
+ const { seeds: _devSeeds, kms: devKms, ...bootFields } = options;
27
+ const composed = withBootOptionEnvBlocks(envSchema, {
28
+ ...bootFields,
29
+ ...(devKms && { kms: devKms }),
30
+ ...(seedsConfig && { seeds: { ...seedsConfig, run: [] } }),
31
+ features: options.features,
32
+ });
33
+ parseEnv(composed.schema, rawEnvSource, { sources: composed.sources });
34
+ }
35
+ function devBootOptionWiringInput(options, seedsConfig) {
36
+ return {
37
+ features: options.features,
38
+ ...(options.mail && { mail: options.mail }),
39
+ ...(options.fileStorage && { fileStorage: options.fileStorage }),
40
+ ...(options.kmsMode !== undefined && { kmsMode: options.kmsMode }),
41
+ ...(options.billing && { billing: options.billing }),
42
+ ...(options.featureToggles && { featureToggles: true }),
43
+ ...(options.demoMode && { demoMode: options.demoMode }),
44
+ ...(seedsConfig && { seeds: { ...seedsConfig, run: [] } }),
45
+ ...(options.kms && { kms: options.kms }),
46
+ ...(options.blindIndexKey !== undefined && { blindIndexKey: options.blindIndexKey }),
47
+ ...(options.auth && { auth: options.auth }),
48
+ };
49
+ }
50
+ function collectDevExtraRoutes(options, unsubscribeRoutes, wiring) {
51
+ return unsubscribeRoutes.length === 0 && wiring.extraRoutes.length === 0
52
+ ? options.extraRoutes
53
+ : [...(options.extraRoutes ?? []), ...unsubscribeRoutes, ...wiring.extraRoutes];
54
+ }
55
+ export async function resolveDevBootConfig(options, bootFailure) {
56
+ // Automatically mixes standard features in auth mode through composeFeatures.
57
+ // runProdApp and the per-app Drizzle schema generator use the same helper,
58
+ // keeping migrations and runtime feature composition aligned.
59
+ const rawEnvSource = options.envSource ?? process.env;
60
+ const { seedsConfig, stackSeeds } = splitDevSeeds(options);
61
+ if (options.envSchema) {
62
+ parseDevEnvSchema(options, seedsConfig, rawEnvSource, options.envSchema);
63
+ }
64
+ rejectRemovedEffectiveFeatures(options);
65
+ const wiring = await resolveBootOptionWiring(devBootOptionWiringInput(options, seedsConfig), rawEnvSource);
66
+ bootFailure.release = () => wiring.kms.close();
67
+ const envSource = withEnvDefaults(wiring.envDefaults, rawEnvSource);
68
+ assertStackBootRequirements({
69
+ features: options.features,
70
+ ...(options.featureToggles && { featureToggles: true }),
71
+ }, ["featureToggles"]);
72
+ // Normalizes auth.mail into the explicit flow fields, symmetrically with
73
+ // runProdApp. hmacSecret is read from the JWT_SECRET environment value and
74
+ // fails fast when unavailable. From this point on, use effectiveAuth rather
75
+ // than options.auth.
76
+ const effectiveAuth = options.auth
77
+ ? resolveAuthMail(options.auth, requireEnv("JWT_SECRET", envSource, "runDevApp"), envSource, {
78
+ mailConfigured: wiring.mailConfigured,
79
+ })
80
+ : undefined;
81
+ const { unsubscribeLinks, unsubscribeRoutes } = resolveUnsubscribeWiring({
82
+ unsubscribe: options.unsubscribe,
83
+ publicOrigin: undefined,
84
+ mailBaseUrl: effectiveAuth?.mail?.baseUrl,
85
+ envSource,
86
+ warn: (message) => createFallbackLogger("runDevApp").warn(message),
87
+ });
88
+ const extraRoutes = collectDevExtraRoutes(options, unsubscribeRoutes, wiring);
89
+ const features = composeAppFeatures(options.features, wiring.features, effectiveAuth);
90
+ return { stackSeeds, wiring, envSource, effectiveAuth, unsubscribeLinks, extraRoutes, features };
91
+ }
92
+ // An explicitly wired file provider (options.files) satisfies the
93
+ // FILE_STORAGE_PROVIDER boot gate. Passed via the validateBoot env option
94
+ // instead of mutating process.env, so nothing leaks into later runDevApp
95
+ // calls in the same process.
96
+ function resolveDevValidateBootOptions(options, wiring) {
97
+ const baseBootEnv = {
98
+ ...wiring.envDefaults,
99
+ ...(options.validateBootOptions?.env ?? process.env),
100
+ };
101
+ const hasBootEnvDefaults = Object.keys(wiring.envDefaults).length > 0;
102
+ if (options.files !== undefined && baseBootEnv[FILE_STORAGE_PROVIDER_ENV] === undefined) {
103
+ return {
104
+ ...options.validateBootOptions,
105
+ env: { ...baseBootEnv, [FILE_STORAGE_PROVIDER_ENV]: FILE_STORAGE_PROVIDER_BOOT_SENTINEL },
106
+ };
107
+ }
108
+ return hasBootEnvDefaults
109
+ ? { ...options.validateBootOptions, env: baseBootEnv }
110
+ : options.validateBootOptions;
111
+ }
112
+ // Run boot validation before creating the server. This catches the same
113
+ // errors runProdApp catches (unqualified navigation or handler QNs,
114
+ // screen-access violations), which would otherwise only show up as a prod pod
115
+ // in CrashLoopBackOff. It throws
116
+ // synchronously before a socket is opened.
117
+ export function validateDevBoot(options, config) {
118
+ const bootOptions = resolveDevValidateBootOptions(options, config.wiring);
119
+ // Resolve this before validateBoot: an app-supplied masterKey provider must
120
+ // satisfy the encrypted-field gate exactly as it does in runProdApp.
121
+ const bootCrypto = resolveBootCrypto(config.envSource, options.masterKey);
122
+ validateComposedFeatures(config.features, {
123
+ ...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
124
+ ...bootOptions,
125
+ });
126
+ return bootCrypto;
127
+ }
128
+ function resolveDevPiiKmsResolution(options, wiring) {
129
+ return options.kmsMode !== undefined
130
+ ? {
131
+ kms: wiring.kms.kms,
132
+ blindIndexKey: wiring.kms.blindIndexKey,
133
+ generatedBlindIndexKey: false,
134
+ mode: wiring.kms.kms ? "app-kms" : "plaintext",
135
+ }
136
+ : resolveDevPiiKms(options);
137
+ }
138
+ export function configureDevBootCrypto(options, config, bootCrypto, resetLateBindings, bootFailure) {
139
+ const { wiring, features } = config;
140
+ // The app-wide cipher for `encrypted: true` entity fields is resolved lazily
141
+ // by executors, symmetrically with runProdApp.
142
+ configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
143
+ // Configure Subject-KMS and blind-index handling, symmetrically with
144
+ // runProdApp. Dev warns instead of failing for plaintext PII, but kms with
145
+ // lookupable fields still requires blindIndexKey because lookups would fail
146
+ // in every mode.
147
+ const piiKms = resolveDevPiiKmsResolution(options, wiring);
148
+ configurePiiSubjectKms(piiKms.kms);
149
+ configureBlindIndexKey(piiKms.blindIndexKey);
150
+ const releaseEphemeralPiiKms = captureEphemeralDevPiiKms(piiKms);
151
+ // The auth features bind database-backed revokers onto their (possibly shared) instances;
152
+ // stop() swaps them for inert ones so a later stack in the same process never reaches this
153
+ // app's closed connection.
154
+ const releasePiiKms = async () => {
155
+ for (const reset of resetLateBindings)
156
+ reset();
157
+ releaseEphemeralPiiKms();
158
+ await wiring.kms.close();
159
+ };
160
+ bootFailure.release = releasePiiKms;
161
+ assertPiiBootInvariants(features, {
162
+ kms: piiKms.kms,
163
+ blindIndexKey: piiKms.blindIndexKey,
164
+ mode: "dev",
165
+ });
166
+ if (piiKms.mode === "ephemeral-default") {
167
+ logInfo("[runDevApp] PII encrypted with in-memory KMS (ephemeral DB); pass kms: false to opt out");
168
+ }
169
+ return { releasePiiKms };
170
+ }
171
+ // Throwaway registry, built once and reused below — mirrors runProdApp's
172
+ // `createRegistry(features)` right after validateBoot. Symmetric
173
+ // sessionStoreProviderMounted check to runProdApp's (registry.getExtension-
174
+ // Usages, not a raw features.some(...) scan) so dev and prod never ask the
175
+ // same question two different ways again.
176
+ export function resolveDevAuthRegistry(config) {
177
+ const registry = config.effectiveAuth ? createRegistry(config.features) : undefined;
178
+ const sessionStoreProviderMounted = registry !== undefined && registry.getExtensionUsages(EXT_SESSION_STORE).length > 0;
179
+ assertSessionBootInvariants({
180
+ hasAuth: Boolean(config.effectiveAuth),
181
+ sessionStoreProviderMounted,
182
+ mode: "dev",
183
+ });
184
+ return { registry, sessionStoreProviderMounted };
185
+ }
@@ -0,0 +1,13 @@
1
+ import type { CreateKumikoServerOptions } from "./create-kumiko-server.ts";
2
+ import type { RunDevAppOptions } from "./run-dev-app.ts";
3
+ import type { DevAuthWiring } from "./run-dev-app-auth.ts";
4
+ import type { DevBootConfig } from "./run-dev-app-config.ts";
5
+ export type DevServerOptions = Omit<CreateKumikoServerOptions, "onAfterSetup">;
6
+ export declare function buildDevServerOptions(deps: {
7
+ readonly options: RunDevAppOptions;
8
+ readonly config: DevBootConfig;
9
+ readonly auth: DevAuthWiring;
10
+ readonly extraContext: CreateKumikoServerOptions["extraContext"];
11
+ readonly trustedProxyHops: number | undefined;
12
+ readonly effectiveFeatures: CreateKumikoServerOptions["effectiveFeatures"];
13
+ }): DevServerOptions;
@@ -0,0 +1,49 @@
1
+ import { buildSharedAuthRoutes } from "@cosmicdrift/kumiko-server-runtime/boot/shared-boot-steps";
2
+ function pickDevClientOptions(options) {
3
+ return {
4
+ ...(options.clientEntry !== undefined && { clientEntry: options.clientEntry }),
5
+ ...(options.errorDocs && { errorDocs: options.errorDocs }),
6
+ ...(options.clientEntries !== undefined && { clientEntries: options.clientEntries }),
7
+ ...(options.hostDispatch !== undefined && { hostDispatch: options.hostDispatch }),
8
+ ...(options.resolvePageHead !== undefined && { resolvePageHead: options.resolvePageHead }),
9
+ ...(options.stylesheet !== undefined && { stylesheet: options.stylesheet }),
10
+ ...(options.stylesheetWatch !== undefined && { stylesheetWatch: options.stylesheetWatch }),
11
+ ...(options.htmlPath !== undefined && { htmlPath: options.htmlPath }),
12
+ ...(options.port !== undefined && { port: options.port }),
13
+ ...(options.watchDirs !== undefined && { watchDirs: options.watchDirs }),
14
+ ...(options.installSignalHandlers !== undefined && {
15
+ installSignalHandlers: options.installSignalHandlers,
16
+ }),
17
+ };
18
+ }
19
+ function pickDevServiceOptions(options) {
20
+ return {
21
+ ...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
22
+ ...(options.files !== undefined && { files: options.files }),
23
+ ...(options.wire !== undefined && { wire: options.wire }),
24
+ };
25
+ }
26
+ export function buildDevServerOptions(deps) {
27
+ const { options, config, auth, extraContext, trustedProxyHops, effectiveFeatures } = deps;
28
+ const { effectiveAuth, extraRoutes, wiring } = config;
29
+ return {
30
+ features: config.features,
31
+ ...pickDevClientOptions(options),
32
+ ...(extraContext !== undefined && { extraContext }),
33
+ ...pickDevServiceOptions(options),
34
+ ...(extraRoutes !== undefined && { extraRoutes }),
35
+ ...(trustedProxyHops !== undefined && { trustedProxyHops }),
36
+ ...(options.demoMode && { wrapFetch: wiring.wrapFetch }),
37
+ ...(effectiveFeatures !== undefined && { effectiveFeatures }),
38
+ ...(effectiveAuth && {
39
+ auth: {
40
+ ...buildSharedAuthRoutes(effectiveAuth, Boolean(auth.mfaFeature)),
41
+ ...(effectiveAuth.trustedProxyHops !== undefined && {
42
+ trustedProxyHops: effectiveAuth.trustedProxyHops,
43
+ }),
44
+ ...auth.sessionAuthFragment,
45
+ ...auth.patAuthFragment,
46
+ },
47
+ }),
48
+ };
49
+ }