@cosmicdrift/kumiko-dev-server 0.152.0 → 0.154.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/package.json +5 -8
- package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
- package/src/__tests__/compose-stacks.test.ts +1 -1
- package/src/__tests__/discover-format.test.ts +1 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/walkthrough.integration.test.ts +1 -1
- package/src/build.ts +1 -1
- package/src/create-kumiko-server.ts +12 -6
- package/src/index.ts +4 -12
- package/src/run-dev-app.ts +12 -8
- package/src/scaffold-app.ts +9 -4
- package/src/schema-apply.ts +4 -1
- package/src/schema-check-core.ts +1 -1
- package/src/setup-test-stack-from-features.ts +4 -1
- package/src/__tests__/boot-extra-context.test.ts +0 -140
- package/src/__tests__/build-prod-bundle.test.ts +0 -278
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
- package/src/__tests__/compose-features.test.ts +0 -128
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/inject-schema.test.ts +0 -62
- package/src/__tests__/pii-boot-gate.test.ts +0 -68
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/require-env.test.ts +0 -29
- package/src/__tests__/resolve-auth-mail.test.ts +0 -69
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
- package/src/__tests__/run-prod-app-spec.test.ts +0 -57
- package/src/__tests__/run-prod-app.integration.test.ts +0 -915
- package/src/__tests__/session-wiring.test.ts +0 -51
- package/src/__tests__/try-hono-first.test.ts +0 -63
- package/src/boot/__tests__/job-run-logger.test.ts +0 -26
- package/src/boot/apply-boot-seeds.ts +0 -19
- package/src/boot/boot-crypto.ts +0 -82
- package/src/boot/job-run-logger.ts +0 -51
- package/src/build-prod-bundle.ts +0 -692
- package/src/compose-features.ts +0 -164
- package/src/extra-routes-deps.ts +0 -47
- package/src/inject-schema.ts +0 -30
- package/src/pii-boot-gate.ts +0 -62
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app-boot-context.ts +0 -241
- package/src/run-prod-app-static-files.ts +0 -273
- package/src/run-prod-app.ts +0 -1158
- package/src/session-wiring.ts +0 -29
- package/src/try-hono-first.ts +0 -46
package/src/compose-features.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
// composeFeatures — single source of truth für die Feature-Liste die
|
|
2
|
-
// Boot UND Schema-Generator sehen.
|
|
3
|
-
//
|
|
4
|
-
// Sowohl runDevApp als auch runProdApp mischen im auth-mode dieselben
|
|
5
|
-
// vier Bundled-Features dazu (config + user + tenant + auth-email-pw).
|
|
6
|
-
// Damit der drizzle-Schema-Generator pro App genau dieselbe Feature-
|
|
7
|
-
// Liste sieht wie die Runtime, leben die Komposition hier — beide
|
|
8
|
-
// Bootstrap-Wrapper UND der per-app drizzle/generate.ts rufen sie auf.
|
|
9
|
-
//
|
|
10
|
-
// Reihenfolge: Infrastruktur-Features (config/user/tenant) zuerst, dann
|
|
11
|
-
// auth-email-password, dann die App-Features. Spätere Features dürfen
|
|
12
|
-
// auf Frühere referenzieren (z.B. authClaims-Hooks an user/tenant).
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
type AuthEmailPasswordOptions,
|
|
16
|
-
type AuthMailLocale,
|
|
17
|
-
createAuthEmailPasswordFeature,
|
|
18
|
-
type EmailVerificationOptions,
|
|
19
|
-
type InviteOptions,
|
|
20
|
-
type PasswordResetOptions,
|
|
21
|
-
type SignupOptions,
|
|
22
|
-
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
23
|
-
import {
|
|
24
|
-
AUTH_MFA_FEATURE,
|
|
25
|
-
mfaStatusCheckerFromFeature,
|
|
26
|
-
} from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
27
|
-
import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
28
|
-
import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
29
|
-
import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
30
|
-
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
31
|
-
|
|
32
|
-
export type ComposeFeaturesOptions = {
|
|
33
|
-
/** When true, prepends config + user + tenant + auth-email-password
|
|
34
|
-
* before the app features. Mirror of "auth-mode" in run{Dev,Prod}App. */
|
|
35
|
-
readonly includeBundled: boolean;
|
|
36
|
-
/** Optional auth-feature-options durchgereicht an
|
|
37
|
-
* createAuthEmailPasswordFeature. Wenn passwordReset / emailVerification
|
|
38
|
-
* hier gesetzt sind, registriert das Feature die request-/confirm-
|
|
39
|
-
* Handler — sonst NICHT (500 wenn die routes via auth-routes.ts
|
|
40
|
-
* gemounted sind aber kein Handler dispatcht). Hand-in-hand mit dem
|
|
41
|
-
* passwordReset-Block in RunProdAppAuthOptions / RunDevAppAuthOptions. */
|
|
42
|
-
readonly authOptions?: AuthEmailPasswordOptions;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export function composeFeatures(
|
|
46
|
-
appFeatures: readonly FeatureDefinition[],
|
|
47
|
-
options: ComposeFeaturesOptions,
|
|
48
|
-
): FeatureDefinition[] {
|
|
49
|
-
// ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
|
|
50
|
-
// an app composing its own foundation and mounting auth-mfa itself is
|
|
51
|
-
// responsible for threading mfaStatusCheckerFromFeature(...) into its
|
|
52
|
-
// own createAuthEmailPasswordFeature(...) call, or login silently
|
|
53
|
-
// bypasses MFA. Upgrade if this trips someone: warn here when appFeatures
|
|
54
|
-
// contains AUTH_MFA_FEATURE but includeBundled is false.
|
|
55
|
-
if (!options.includeBundled) return [...appFeatures];
|
|
56
|
-
|
|
57
|
-
// Bundled foundation goes first so its instances carry the runDevApp /
|
|
58
|
-
// runProdApp `authOptions` (passwordReset wiring etc.). App-features that
|
|
59
|
-
// ALSO declare one of these names — e.g. the create-kumiko-app picker
|
|
60
|
-
// hands back `createAuthEmailPasswordFeature()` because the user ticked
|
|
61
|
-
// it — would otherwise crash createRegistry with "Duplicate feature".
|
|
62
|
-
// Drop the app-side duplicates and warn so the user can clean run-config.
|
|
63
|
-
//
|
|
64
|
-
// auth-mfa is NOT part of the bundled foundation (apps opt in explicitly
|
|
65
|
-
// via APP_FEATURES) — but if it's there, the login handler needs its
|
|
66
|
-
// status-checker wired in at construction time, since createAuthEmail-
|
|
67
|
-
// PasswordFeature is built right here, before the caller ever sees it.
|
|
68
|
-
const mfaFeature = appFeatures.find((f) => f.name === AUTH_MFA_FEATURE);
|
|
69
|
-
const authOptions = mfaFeature
|
|
70
|
-
? { ...options.authOptions, mfaStatusChecker: mfaStatusCheckerFromFeature(mfaFeature) }
|
|
71
|
-
: options.authOptions;
|
|
72
|
-
const bundled = [
|
|
73
|
-
createConfigFeature(),
|
|
74
|
-
createUserFeature(),
|
|
75
|
-
createTenantFeature(),
|
|
76
|
-
createAuthEmailPasswordFeature(authOptions ?? {}),
|
|
77
|
-
];
|
|
78
|
-
const bundledNames = new Set(bundled.map((f) => f.name));
|
|
79
|
-
const filteredApp: FeatureDefinition[] = [];
|
|
80
|
-
for (const f of appFeatures) {
|
|
81
|
-
if (bundledNames.has(f.name)) {
|
|
82
|
-
// biome-ignore lint/suspicious/noConsole: boot-time UX warning
|
|
83
|
-
console.warn(
|
|
84
|
-
`[composeFeatures] "${f.name}" already auto-mounted via includeBundled — dropping the explicit copy from APP_FEATURES. Remove it from run-config.ts to silence this warning.`,
|
|
85
|
-
);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
filteredApp.push(f);
|
|
89
|
-
}
|
|
90
|
-
return [...bundled, ...filteredApp];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Shape eines beliebigen run{Prod,Dev}App-Auth-Blocks der eine
|
|
94
|
-
* PasswordReset/EmailVerification-Konfiguration tragen kann. Die
|
|
95
|
-
* Wrapper-API (PasswordResetSetup) extends die Feature-API
|
|
96
|
-
* (PasswordResetOptions), darum reicht ein structural-typed
|
|
97
|
-
* Lookup auf den auth-only-Subset. Erlaubt buildComposeAuthOptions
|
|
98
|
-
* mit RunProd- UND RunDev-AuthOptions zu callen ohne den Helper
|
|
99
|
-
* doppelt zu bauen. */
|
|
100
|
-
export type AuthOptionsCarrier = {
|
|
101
|
-
readonly passwordReset?: PasswordResetOptions;
|
|
102
|
-
readonly emailVerification?: EmailVerificationOptions;
|
|
103
|
-
readonly signup?: SignupOptions;
|
|
104
|
-
readonly invite?: InviteOptions;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/** Baut den authOptions-Block für composeFeatures aus einem
|
|
108
|
-
* Wrapper-Auth-Block. Reicht NUR die feature-side-Felder
|
|
109
|
-
* (hmacSecret, tokenTtlMinutes, mode) durch — die mail-side
|
|
110
|
-
* (sendResetEmail/appResetUrl) gehört in die auth-routes-config
|
|
111
|
-
* und wird vom Wrapper separat verdrahtet.
|
|
112
|
-
*
|
|
113
|
-
* Returnt undefined wenn weder passwordReset noch emailVerification
|
|
114
|
-
* gesetzt sind (composeFeatures default-deny: KEINE handler in der
|
|
115
|
-
* Registry registriert, /api/auth/request-password-reset etc. bleiben
|
|
116
|
-
* 401/404). */
|
|
117
|
-
type MailFlowFields = {
|
|
118
|
-
readonly appUrl: string;
|
|
119
|
-
readonly tokenTtlMinutes?: number;
|
|
120
|
-
readonly appName?: string;
|
|
121
|
-
readonly locale?: AuthMailLocale;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// The magic-link mail fields shared by all four flows. Conditional spreads omit
|
|
125
|
-
// undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
|
|
126
|
-
// trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
|
|
127
|
-
// reset/verify layer hmacSecret (+ mode) on top.
|
|
128
|
-
function pickMailFields(src: MailFlowFields): MailFlowFields {
|
|
129
|
-
return {
|
|
130
|
-
appUrl: src.appUrl,
|
|
131
|
-
...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
|
|
132
|
-
...(src.appName !== undefined && { appName: src.appName }),
|
|
133
|
-
...(src.locale !== undefined && { locale: src.locale }),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function buildComposeAuthOptions(
|
|
138
|
-
auth: AuthOptionsCarrier | undefined,
|
|
139
|
-
): AuthEmailPasswordOptions | undefined {
|
|
140
|
-
if (!auth) return undefined;
|
|
141
|
-
const opts: { -readonly [K in keyof AuthEmailPasswordOptions]: AuthEmailPasswordOptions[K] } = {};
|
|
142
|
-
if (auth.passwordReset) {
|
|
143
|
-
opts.passwordReset = {
|
|
144
|
-
hmacSecret: auth.passwordReset.hmacSecret,
|
|
145
|
-
...pickMailFields(auth.passwordReset),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
if (auth.emailVerification) {
|
|
149
|
-
opts.emailVerification = {
|
|
150
|
-
hmacSecret: auth.emailVerification.hmacSecret,
|
|
151
|
-
...pickMailFields(auth.emailVerification),
|
|
152
|
-
...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
if (auth.signup) {
|
|
156
|
-
opts.signup = pickMailFields(auth.signup);
|
|
157
|
-
}
|
|
158
|
-
if (auth.invite) {
|
|
159
|
-
opts.invite = pickMailFields(auth.invite);
|
|
160
|
-
}
|
|
161
|
-
return opts.passwordReset || opts.emailVerification || opts.signup || opts.invite
|
|
162
|
-
? opts
|
|
163
|
-
: undefined;
|
|
164
|
-
}
|
package/src/extra-routes-deps.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
2
|
-
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
4
|
-
createSystemUser,
|
|
5
|
-
type Registry,
|
|
6
|
-
type SessionUser,
|
|
7
|
-
type TenantId,
|
|
8
|
-
type WriteResult,
|
|
9
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
-
import type Redis from "ioredis";
|
|
11
|
-
|
|
12
|
-
/** Deps für `extraRoutes` — geteilt zwischen runProdApp (prod) und
|
|
13
|
-
* createKumikoServer (dev), damit die beiden Pfade nicht driften.
|
|
14
|
-
* Naming: `deps` statt `ctx` weil im Framework `ctx` der HandlerContext
|
|
15
|
-
* mit user/tenant/registry ist — hier ist der Scope absichtlich kleiner
|
|
16
|
-
* (Routes laufen außerhalb der Auth/Tenant-Pipeline). */
|
|
17
|
-
export type ExtraRoutesSystemDeps = {
|
|
18
|
-
readonly db: DbConnection;
|
|
19
|
-
readonly redis: Redis;
|
|
20
|
-
/** Feature-registry — z.B. für Plugin-Lookups via
|
|
21
|
-
* `registry.getExtensionUsages("subscriptionProvider")`. */
|
|
22
|
-
readonly registry: Registry;
|
|
23
|
-
/** Schreibt durch den /api/*-Command-Dispatcher (gleiche Idempotency/
|
|
24
|
-
* Job-Hooks) — aber als auto-konstruierter SystemAdmin des Ziel-
|
|
25
|
-
* Tenants, OHNE Access-Check der Route. Privilege-Scope: SystemAdmin
|
|
26
|
-
* ist die höchste nicht-tenant-scoped Rolle — der Call erreicht JEDEN
|
|
27
|
-
* SystemAdmin-gegateten Handler auf jedem Tenant; das Rollen-Set ist
|
|
28
|
-
* nicht konfigurierbar. Nur für Pfade, die ihre Authentizität selbst
|
|
29
|
-
* beweisen (Provider-Webhook-Signaturen,
|
|
30
|
-
* createSubscriptionWebhookHandler et al.). */
|
|
31
|
-
readonly dispatchSystemWrite: (args: {
|
|
32
|
-
readonly handlerQn: string;
|
|
33
|
-
readonly payload: unknown;
|
|
34
|
-
readonly tenantId: TenantId;
|
|
35
|
-
}) => Promise<WriteResult>;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
type SystemWriteDispatcher = {
|
|
39
|
-
readonly write: (handlerQn: string, payload: unknown, user: SessionUser) => Promise<WriteResult>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export function makeDispatchSystemWrite(
|
|
43
|
-
dispatcher: SystemWriteDispatcher,
|
|
44
|
-
): ExtraRoutesSystemDeps["dispatchSystemWrite"] {
|
|
45
|
-
return ({ handlerQn, payload, tenantId }) =>
|
|
46
|
-
dispatcher.write(handlerQn, payload, createSystemUser(tenantId, [ROLES.SystemAdmin]));
|
|
47
|
-
}
|
package/src/inject-schema.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Injiziert das Server-aufgelöste AppSchema in das HTML-Template damit
|
|
2
|
-
// createKumikoApp() es synchron unter `window.__KUMIKO_SCHEMA__` vorfindet.
|
|
3
|
-
// JSON ist valides JS — direkt eingebettet, der Browser parsed das
|
|
4
|
-
// Object-Literal nativ.
|
|
5
|
-
//
|
|
6
|
-
// Geteilt zwischen dev-server (Schema kommt frisch aus dem laufenden
|
|
7
|
-
// Prozess) und prod-server (Schema wird beim Boot einmal berechnet und
|
|
8
|
-
// in die statisch ausgelieferte index.html injiziert). Beide Pfade
|
|
9
|
-
// nutzen dieselbe Tag-Form damit `createKumikoApp` den Lookup nicht je
|
|
10
|
-
// nach Kontext anders machen muss.
|
|
11
|
-
//
|
|
12
|
-
// Idempotenz: wenn das HTML schon einen __KUMIKO_SCHEMA__-Marker hat,
|
|
13
|
-
// wird nicht doppelt injected — verhindert dass repeated index.html-
|
|
14
|
-
// Reads (prod) oder bereits-vorbereitete templates (custom CI-builds)
|
|
15
|
-
// stacking-Tags produzieren.
|
|
16
|
-
|
|
17
|
-
// `<` als < serialisieren: verhindert `</script>`-Breakout aus dem
|
|
18
|
-
// RAWTEXT-Block, JSON bleibt valides JS.
|
|
19
|
-
function scriptSafeJsonHtml(json: string): string {
|
|
20
|
-
return json.replace(/</g, "\\u003c");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function injectSchema(html: string, schemaJson: string): string {
|
|
24
|
-
if (html.includes("__KUMIKO_SCHEMA__")) return html;
|
|
25
|
-
const tag = `<script>window.__KUMIKO_SCHEMA__=${scriptSafeJsonHtml(schemaJson)};</script>`;
|
|
26
|
-
if (html.includes('<script src="/client.js"')) {
|
|
27
|
-
return html.replace('<script src="/client.js"', `${tag}<script src="/client.js"`);
|
|
28
|
-
}
|
|
29
|
-
return html.includes("</body>") ? html.replace("</body>", `${tag}</body>`) : html + tag;
|
|
30
|
-
}
|
package/src/pii-boot-gate.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collectLookupableFields,
|
|
3
|
-
collectPiiSubjectFields,
|
|
4
|
-
type KmsAdapter,
|
|
5
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
6
|
-
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
-
|
|
8
|
-
type PiiGateOptions = {
|
|
9
|
-
readonly kms?: KmsAdapter | undefined;
|
|
10
|
-
readonly blindIndexKey?: string | undefined;
|
|
11
|
-
readonly allowPlaintextPii?: string | undefined;
|
|
12
|
-
/** prod fails hard on plaintext PII (opt-out via allowPlaintextPii);
|
|
13
|
-
* dev only warns — local data, and an InMemory KMS against a persistent
|
|
14
|
-
* dev DB would strand every row after a restart. */
|
|
15
|
-
readonly mode: "prod" | "dev";
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function assertPiiBootInvariants(
|
|
19
|
-
features: readonly FeatureDefinition[],
|
|
20
|
-
opts: PiiGateOptions,
|
|
21
|
-
): void {
|
|
22
|
-
const tag = opts.mode === "prod" ? "runProdApp" : "runDevApp";
|
|
23
|
-
|
|
24
|
-
const lookupableEntities = features.flatMap((feature) =>
|
|
25
|
-
Object.entries(feature.entities ?? {})
|
|
26
|
-
.filter(([, entity]) => collectLookupableFields(entity).length > 0)
|
|
27
|
-
.map(([name]) => name),
|
|
28
|
-
);
|
|
29
|
-
if (opts.kms && !opts.blindIndexKey && lookupableEntities.length > 0) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`[${tag}] BOOT ABORTED — entities [${lookupableEntities.join(", ")}] declare lookupable fields and a KMS is configured, but no blindIndexKey was passed. Equality lookups on encrypted fields would silently stop matching. Pass { blindIndexKey } (env: KUMIKO_BLIND_INDEX_KEY, generate: openssl rand -base64 32).`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// skip: KMS configured — PII fields are encrypted, nothing left to gate.
|
|
36
|
-
if (opts.kms) return;
|
|
37
|
-
const piiEntities = features.flatMap((feature) =>
|
|
38
|
-
Object.entries(feature.entities ?? {})
|
|
39
|
-
.filter(([, entity]) => collectPiiSubjectFields(entity).length > 0)
|
|
40
|
-
.map(([name]) => name),
|
|
41
|
-
);
|
|
42
|
-
// skip: no PII-annotated entities mounted — plaintext gate is moot.
|
|
43
|
-
if (piiEntities.length === 0) return;
|
|
44
|
-
|
|
45
|
-
if (opts.mode === "dev") {
|
|
46
|
-
// biome-ignore lint/suspicious/noConsole: boot-time security warning
|
|
47
|
-
console.warn(
|
|
48
|
-
`[${tag}] ${piiEntities.length} entities carry pii/userOwned/tenantOwned annotations but no \`kms\` adapter is configured — fields are stored in PLAINTEXT locally. Pass { kms: new InMemoryKmsAdapter() } (ephemeral DB) or createPgKmsAdapter(...) to exercise crypto-shredding in dev.`,
|
|
49
|
-
);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (opts.allowPlaintextPii) {
|
|
53
|
-
// biome-ignore lint/suspicious/noConsole: boot-time security warning
|
|
54
|
-
console.warn(
|
|
55
|
-
`[${tag}] ${piiEntities.length} entities carry PII annotations but no \`kms\` adapter is configured — fields are stored in PLAINTEXT (allowPlaintextPii: "${opts.allowPlaintextPii}"). GDPR erasure via crypto-shredding is NOT possible until a KMS is provisioned.`,
|
|
56
|
-
);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
throw new Error(
|
|
60
|
-
`[${tag}] BOOT ABORTED — entities [${piiEntities.join(", ")}] carry pii/userOwned/tenantOwned annotations but no \`kms\` adapter is configured. The fields would be stored in PLAINTEXT and GDPR erasure (crypto-shredding) could not work. Pass runProdApp({ kms: createPgKmsAdapter({ databaseUrl, platformKek }) }) — or acknowledge explicitly with { allowPlaintextPii: "<reason>" } until your KMS is provisioned.`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Resolved den lokal installierten @tailwindcss/cli-Bin auf seinen
|
|
2
|
-
// absoluten Dateipfad. Wir vermeiden `bunx`, weil das auch bei lokal
|
|
3
|
-
// installiertem Package noch das Registry-Manifest fragt und ohne Netz
|
|
4
|
-
// mit `FailedToOpenSocket` stirbt.
|
|
5
|
-
//
|
|
6
|
-
// Ausgelagert aus create-kumiko-server.ts, damit unter vitest/Node
|
|
7
|
-
// testbar (Bun-Branch + Resolve-Fail-Branch) ohne den Server zu
|
|
8
|
-
// booten.
|
|
9
|
-
|
|
10
|
-
import { dirname, resolve } from "node:path";
|
|
11
|
-
|
|
12
|
-
type BunResolver = { resolveSync: (id: string, from: string) => string };
|
|
13
|
-
|
|
14
|
-
export type ResolveTailwindCliDeps = {
|
|
15
|
-
readonly bun?: BunResolver;
|
|
16
|
-
readonly cwd: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function resolveTailwindCli(deps: ResolveTailwindCliDeps): string | undefined {
|
|
20
|
-
if (deps.bun === undefined) return undefined;
|
|
21
|
-
try {
|
|
22
|
-
const pkgJsonPath = deps.bun.resolveSync("@tailwindcss/cli/package.json", deps.cwd);
|
|
23
|
-
// bin: { tailwindcss: "./dist/index.mjs" } → absoluter Pfad
|
|
24
|
-
return resolve(pkgJsonPath, "..", "dist", "index.mjs");
|
|
25
|
-
} catch {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
|
|
31
|
-
* CSS file. Skip renderer-web fallback (or fail build) when peer deps
|
|
32
|
-
* aren't resolvable from that directory — e.g. Bun cache copies of
|
|
33
|
-
* `@cosmicdrift/kumiko-renderer-web` outside a hoisted node_modules tree. */
|
|
34
|
-
export function canResolveTailwindStylesheet(
|
|
35
|
-
entryCss: string,
|
|
36
|
-
deps: ResolveTailwindCliDeps,
|
|
37
|
-
): boolean {
|
|
38
|
-
if (deps.bun === undefined) return false;
|
|
39
|
-
try {
|
|
40
|
-
deps.bun.resolveSync("tailwindcss", dirname(entryCss));
|
|
41
|
-
return true;
|
|
42
|
-
} catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { makeAuthPaths } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
2
|
-
import { bindMfaRevokeAllOtherSessionsFromFeature } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
3
|
-
import { createSmtpTransportFromEnv } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
4
|
-
import {
|
|
5
|
-
buildEnvConfigOverrides,
|
|
6
|
-
createConfigAccessorFactory,
|
|
7
|
-
createConfigResolver,
|
|
8
|
-
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
9
|
-
import {
|
|
10
|
-
collectChannels,
|
|
11
|
-
createDeliveryService,
|
|
12
|
-
DELIVERY_FEATURE,
|
|
13
|
-
} from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
14
|
-
import {
|
|
15
|
-
createSecretsContext,
|
|
16
|
-
SECRETS_FEATURE_NAME,
|
|
17
|
-
} from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
18
|
-
import {
|
|
19
|
-
bindAutoRevokeFromFeature,
|
|
20
|
-
createSessionCallbacks,
|
|
21
|
-
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
22
|
-
import { createTextContentApi } from "@cosmicdrift/kumiko-bundled-features/text-content";
|
|
23
|
-
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
24
|
-
import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
25
|
-
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
26
|
-
import type {
|
|
27
|
-
ConfigResolver,
|
|
28
|
-
FeatureDefinition,
|
|
29
|
-
NotifyFactory,
|
|
30
|
-
Registry,
|
|
31
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
32
|
-
import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
33
|
-
import { type BootCrypto, resolveBootCrypto } from "./boot/boot-crypto";
|
|
34
|
-
import type {
|
|
35
|
-
AuthMailOptions,
|
|
36
|
-
EmailVerificationSetup,
|
|
37
|
-
InviteSetup,
|
|
38
|
-
PasswordResetSetup,
|
|
39
|
-
SignupSetup,
|
|
40
|
-
} from "./run-prod-app";
|
|
41
|
-
import type { ProdSessionsConfig } from "./session-wiring";
|
|
42
|
-
|
|
43
|
-
// Boot-time context helpers for runProdApp: ctx-extra-context wiring
|
|
44
|
-
// (textContent/delivery/secrets/config-resolver), auth-mail convenience
|
|
45
|
-
// normalization, and prod session-auth wiring. Split out of run-prod-app.ts
|
|
46
|
-
// (#1005, Welle 2) — mechanical relocation, these are self-contained pure
|
|
47
|
-
// functions, no closure over runProdApp's local boot state.
|
|
48
|
-
|
|
49
|
-
// Shared with runDevApp (mergeConfigResolverDefault) for dev/prod parity.
|
|
50
|
-
export function addConfigAccessorFactory<T extends { readonly configResolver?: ConfigResolver }>(
|
|
51
|
-
resolved: T,
|
|
52
|
-
registry: Registry,
|
|
53
|
-
): T {
|
|
54
|
-
if (!resolved.configResolver) return resolved;
|
|
55
|
-
return {
|
|
56
|
-
...resolved,
|
|
57
|
-
_configAccessorFactory: createConfigAccessorFactory(registry, resolved.configResolver),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Framework-Default-Provider für den AppContext — gleicher Mechanismus wie
|
|
62
|
-
// der tenantTierResolver-Autowire (findTierResolverUsage): deklarierter
|
|
63
|
-
// Bedarf (Feature gemountet) → Default aus db/env, App überschreibt nur die
|
|
64
|
-
// Ausnahme. textContent ist unbedingt (createTextContentApi wirft nie, baut
|
|
65
|
-
// nur einen db-gebundenen Accessor). secrets wird nur auto-verdrahtet wenn
|
|
66
|
-
// das secrets-Feature gemountet ist UND ein KEK tatsächlich verfügbar ist
|
|
67
|
-
// (masterKey-Override ODER env-KEK present) — sonst skip, damit der eager
|
|
68
|
-
// KEK-Detection + Provider/Cipher-Aufbau leben in boot/boot-crypto.ts
|
|
69
|
-
// (envHasMasterKek, resolveBootCrypto) — gemeinsam mit runDevApp.
|
|
70
|
-
// Prod-Misconfig (secrets gemountet, kein KEK) fängt schon secretsEnvSchema
|
|
71
|
-
// beim Boot; fehlt der env-Schema-Pfad, wirft requireSecretsContext beim
|
|
72
|
-
// ersten ctx.secrets-Zugriff mit Wiring-Hinweis. configResolver nur im
|
|
73
|
-
// Auth-Mode. Exportiert + pure für Unit-Tests; der merge mit App-Werten
|
|
74
|
-
// passiert beim Caller (App gewinnt).
|
|
75
|
-
|
|
76
|
-
// Prod/dev parity for ctx.notify: without this `_notifyFactory` is only wired
|
|
77
|
-
// in tests (createDeliveryTestContext), so ctx.notify is undefined at runtime
|
|
78
|
-
// and every notification silently skips. sseBroker optional (email/push don't
|
|
79
|
-
// need it, in-app SSE does); no jobRunner → queued channels send inline.
|
|
80
|
-
function buildDeliveryNotifyFactory(opts: {
|
|
81
|
-
readonly db: DbConnection;
|
|
82
|
-
readonly registry: Registry;
|
|
83
|
-
readonly sseBroker?: SseBroker;
|
|
84
|
-
}): NotifyFactory {
|
|
85
|
-
const deliveryService = createDeliveryService({
|
|
86
|
-
db: opts.db,
|
|
87
|
-
registry: opts.registry,
|
|
88
|
-
channels: collectChannels(opts.registry),
|
|
89
|
-
...(opts.sseBroker && { sseBroker: opts.sseBroker }),
|
|
90
|
-
});
|
|
91
|
-
return (user, tenantId) => (notificationType, options) =>
|
|
92
|
-
deliveryService.notify(notificationType, options, user, tenantId);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function buildBootExtraContext(opts: {
|
|
96
|
-
readonly db: DbConnection;
|
|
97
|
-
readonly features: readonly FeatureDefinition[];
|
|
98
|
-
readonly envSource: Record<string, string | undefined>;
|
|
99
|
-
readonly registry: Registry;
|
|
100
|
-
readonly hasAuth: boolean;
|
|
101
|
-
// Resolved once per boot via resolveBootCrypto — shared by secrets,
|
|
102
|
-
// config-resolver, config-set-handler and boot-seeds. Absent (tests
|
|
103
|
-
// that don't care about encryption) ⇒ resolved from envSource here.
|
|
104
|
-
readonly crypto?: BootCrypto;
|
|
105
|
-
readonly masterKey?: MasterKeyProvider;
|
|
106
|
-
readonly sseBroker?: SseBroker;
|
|
107
|
-
readonly kms?: KmsAdapter;
|
|
108
|
-
}): Record<string, unknown> {
|
|
109
|
-
const crypto = opts.crypto ?? resolveBootCrypto(opts.envSource, opts.masterKey);
|
|
110
|
-
const hasSecretsFeature = opts.features.some((f) => f.name === SECRETS_FEATURE_NAME);
|
|
111
|
-
const wireSecrets = hasSecretsFeature && crypto.masterKeyProvider !== undefined;
|
|
112
|
-
const hasDeliveryFeature = opts.features.some((f) => f.name === DELIVERY_FEATURE);
|
|
113
|
-
return {
|
|
114
|
-
textContent: createTextContentApi(opts.db),
|
|
115
|
-
...(opts.kms && { kms: opts.kms }),
|
|
116
|
-
...(hasDeliveryFeature && {
|
|
117
|
-
_notifyFactory: buildDeliveryNotifyFactory({
|
|
118
|
-
db: opts.db,
|
|
119
|
-
registry: opts.registry,
|
|
120
|
-
...(opts.sseBroker && { sseBroker: opts.sseBroker }),
|
|
121
|
-
}),
|
|
122
|
-
}),
|
|
123
|
-
// Top-level provider so feature jobs (secrets rotate, config reencrypt)
|
|
124
|
-
// reach it via ctx — previously only test-stack wired it.
|
|
125
|
-
...(crypto.masterKeyProvider && { masterKeyProvider: crypto.masterKeyProvider }),
|
|
126
|
-
// Encrypt/decrypt partner for `encrypted: true` config keys. Wired
|
|
127
|
-
// whenever a master key exists — NOT gated on the secrets feature,
|
|
128
|
-
// config encryption must work without mounting ctx.secrets.
|
|
129
|
-
...(crypto.configCipher && { configEncryption: crypto.configCipher }),
|
|
130
|
-
...(wireSecrets &&
|
|
131
|
-
crypto.masterKeyProvider && {
|
|
132
|
-
secrets: createSecretsContext({
|
|
133
|
-
db: opts.db,
|
|
134
|
-
masterKeyProvider: crypto.masterKeyProvider,
|
|
135
|
-
dekCache: crypto.dekCache,
|
|
136
|
-
}),
|
|
137
|
-
}),
|
|
138
|
-
...(opts.hasAuth && {
|
|
139
|
-
configResolver: createConfigResolver({
|
|
140
|
-
appOverrides: buildEnvConfigOverrides(opts.registry, opts.envSource),
|
|
141
|
-
...(crypto.configCipher && { cipher: crypto.configCipher }),
|
|
142
|
-
}),
|
|
143
|
-
}),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// auth.mail-Convenience → normalisiert in die expliziten passwordReset/
|
|
148
|
-
// emailVerification/signup/invite-Felder, BEVOR buildComposeAuthOptions
|
|
149
|
-
// (Feature-Side: hmacSecret/mode) und das auth-routes-Fragment sie lesen —
|
|
150
|
-
// so speist EIN mail-Block beide Pfade. App-explizite Flows gewinnen über
|
|
151
|
-
// den Default. Null-Transport-Guard: ohne SMTP_HOST-env bleibt alles
|
|
152
|
-
// unverdrahtet (sonst lieferten die reset/verify-Routes 500).
|
|
153
|
-
/** Die Auth-Felder die resolveAuthMail liest/normalisiert — beide
|
|
154
|
-
* App-Auth-Typen (prod + dev) erfüllen das strukturell. */
|
|
155
|
-
type AuthMailNormalizable = {
|
|
156
|
-
readonly mail?: AuthMailOptions;
|
|
157
|
-
readonly passwordReset?: PasswordResetSetup;
|
|
158
|
-
readonly emailVerification?: EmailVerificationSetup;
|
|
159
|
-
readonly signup?: SignupSetup;
|
|
160
|
-
readonly invite?: InviteSetup;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
export function resolveAuthMail<T extends AuthMailNormalizable>(
|
|
164
|
-
auth: T,
|
|
165
|
-
hmacSecret: string,
|
|
166
|
-
envSource: Record<string, string | undefined>,
|
|
167
|
-
): T {
|
|
168
|
-
if (!auth.mail) return auth;
|
|
169
|
-
// SMTP-presence gate: ohne SMTP_HOST-env wird KEIN Flow verdrahtet (Routes
|
|
170
|
-
// blieben sonst 500). Der eigentliche Mail-Versand läuft über delivery
|
|
171
|
-
// (channel-email), nicht über diesen Transport — er ist nur der Detektor
|
|
172
|
-
// "ist Mail konfiguriert?".
|
|
173
|
-
if (
|
|
174
|
-
!createSmtpTransportFromEnv(envSource, { fallbackFrom: auth.mail.from ?? "noreply@localhost" })
|
|
175
|
-
) {
|
|
176
|
-
return auth;
|
|
177
|
-
}
|
|
178
|
-
const paths = makeAuthPaths(auth.mail.paths);
|
|
179
|
-
// appName/locale fließen in alle vier Flow-Options (alle mailen via delivery).
|
|
180
|
-
const mailPresentation = {
|
|
181
|
-
...(auth.mail.appName !== undefined && { appName: auth.mail.appName }),
|
|
182
|
-
...(auth.mail.locale !== undefined && { locale: auth.mail.locale }),
|
|
183
|
-
};
|
|
184
|
-
return {
|
|
185
|
-
...auth,
|
|
186
|
-
passwordReset: auth.passwordReset ?? {
|
|
187
|
-
hmacSecret,
|
|
188
|
-
appUrl: `${auth.mail.baseUrl}${paths.resetPassword}`,
|
|
189
|
-
...mailPresentation,
|
|
190
|
-
},
|
|
191
|
-
emailVerification: auth.emailVerification ?? {
|
|
192
|
-
hmacSecret,
|
|
193
|
-
appUrl: `${auth.mail.baseUrl}${paths.verifyEmail}`,
|
|
194
|
-
...(auth.mail.emailVerificationMode !== undefined && {
|
|
195
|
-
mode: auth.mail.emailVerificationMode,
|
|
196
|
-
}),
|
|
197
|
-
...mailPresentation,
|
|
198
|
-
},
|
|
199
|
-
signup: auth.signup ?? {
|
|
200
|
-
appUrl: `${auth.mail.baseUrl}${paths.signupComplete}`,
|
|
201
|
-
...mailPresentation,
|
|
202
|
-
},
|
|
203
|
-
invite: auth.invite ?? {
|
|
204
|
-
appUrl: `${auth.mail.baseUrl}${paths.inviteAccept}`,
|
|
205
|
-
...mailPresentation,
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function buildProdSessionAuth(
|
|
211
|
-
db: DbConnection,
|
|
212
|
-
opts: ProdSessionsConfig,
|
|
213
|
-
sessionsFeature: FeatureDefinition | undefined,
|
|
214
|
-
mfaFeature: FeatureDefinition | undefined,
|
|
215
|
-
): {
|
|
216
|
-
readonly sessionCreator: ReturnType<typeof createSessionCallbacks>["sessionCreator"];
|
|
217
|
-
readonly sessionRevoker: ReturnType<typeof createSessionCallbacks>["sessionRevoker"];
|
|
218
|
-
readonly sessionChecker: ReturnType<typeof createSessionCallbacks>["sessionChecker"];
|
|
219
|
-
readonly sessionStrictMode: true;
|
|
220
|
-
} {
|
|
221
|
-
const cbs = createSessionCallbacks({
|
|
222
|
-
db,
|
|
223
|
-
...(opts.expiresInMs !== undefined && { expiresInMs: opts.expiresInMs }),
|
|
224
|
-
});
|
|
225
|
-
// Secure-by-default: password-change/-reset mass-revokes the user's live
|
|
226
|
-
// sessions without the app opting in via autoRevokeOnPasswordChange.
|
|
227
|
-
if (sessionsFeature) {
|
|
228
|
-
bindAutoRevokeFromFeature(sessionsFeature)?.(cbs.sessionMassRevoker);
|
|
229
|
-
}
|
|
230
|
-
// MFA enable/disable/regenerate mass-revokes every OTHER live session
|
|
231
|
-
// (stolen-session defense) — only wired when auth-mfa is mounted.
|
|
232
|
-
if (mfaFeature) {
|
|
233
|
-
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(cbs.sessionRevokeAllOthers);
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
sessionCreator: cbs.sessionCreator,
|
|
237
|
-
sessionRevoker: cbs.sessionRevoker,
|
|
238
|
-
sessionChecker: cbs.sessionChecker,
|
|
239
|
-
sessionStrictMode: true,
|
|
240
|
-
};
|
|
241
|
-
}
|