@cosmicdrift/kumiko-dev-server 0.387.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.
- package/dist/build-server-bundle.d.ts +1 -1
- package/dist/build-server-bundle.js +2 -2
- package/dist/create-kumiko-server-client-bundles.d.ts +21 -0
- package/dist/create-kumiko-server-client-bundles.js +97 -0
- package/dist/create-kumiko-server-html.d.ts +21 -0
- package/dist/create-kumiko-server-html.js +132 -0
- package/dist/create-kumiko-server-public-files.d.ts +6 -0
- package/dist/create-kumiko-server-public-files.js +96 -0
- package/dist/create-kumiko-server-reload.d.ts +14 -0
- package/dist/create-kumiko-server-reload.js +73 -0
- package/dist/create-kumiko-server-routes.d.ts +16 -0
- package/dist/create-kumiko-server-routes.js +206 -0
- package/dist/create-kumiko-server-shutdown.d.ts +14 -0
- package/dist/create-kumiko-server-shutdown.js +72 -0
- package/dist/create-kumiko-server-stack.d.ts +12 -0
- package/dist/create-kumiko-server-stack.js +101 -0
- package/dist/create-kumiko-server-stylesheet.d.ts +10 -0
- package/dist/create-kumiko-server-stylesheet.js +137 -0
- package/dist/create-kumiko-server-watcher.d.ts +22 -0
- package/dist/create-kumiko-server-watcher.js +155 -0
- package/dist/create-kumiko-server.d.ts +6 -20
- package/dist/create-kumiko-server.js +59 -1046
- package/dist/dev-server-runtime.d.ts +5 -0
- package/dist/dev-server-runtime.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/managed-files/app-jobs.js +1 -1
- package/dist/managed-files/check.js +1 -1
- package/dist/run-dev-app-auth.d.ts +38 -0
- package/dist/run-dev-app-auth.js +155 -0
- package/dist/run-dev-app-config.d.ts +26 -0
- package/dist/run-dev-app-config.js +185 -0
- package/dist/run-dev-app-server-options.d.ts +13 -0
- package/dist/run-dev-app-server-options.js +49 -0
- package/dist/run-dev-app.d.ts +4 -4
- package/dist/run-dev-app.js +86 -459
- package/dist/schema-apply.d.ts +1 -1
- package/dist/schema-apply.js +4 -4
- package/dist/write-server-lockfile.js +23 -16
- package/package.json +9 -9
- package/src/changes.json +30 -0
- 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);
|
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,
|
|
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
|
@@ -18,7 +18,9 @@ export { composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-fea
|
|
|
18
18
|
// the Bun toolchain bundle out of production imports from the main barrel (for
|
|
19
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,
|
|
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_-]+)
|
|
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
|
+
}
|
package/dist/run-dev-app.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
2
2
|
import type { PostAuthLandingResolver } from "@cosmicdrift/kumiko-framework/api";
|
|
3
|
-
import {
|
|
3
|
+
import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
4
4
|
import { type FeatureDefinition, type Registry, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
-
import {
|
|
5
|
+
import type { ComposedEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
6
6
|
import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
7
|
import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
8
8
|
import type { PageHeadResolver } from "@cosmicdrift/kumiko-renderer/headless/apex";
|
|
9
|
-
import {
|
|
9
|
+
import type { BillingBootOption, DemoModeOption, FileStorageBootOption, KmsMode, MailBootOption, ProdSeedsConfig } from "@cosmicdrift/kumiko-server-runtime";
|
|
10
10
|
import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";
|
|
11
11
|
import { type CreateKumikoServerOptions, type KumikoServerHandle } from "./create-kumiko-server.ts";
|
|
12
12
|
export type { AccountLockoutSetup, AccountUnlockSetup, AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|