@cosmicdrift/kumiko-server-runtime 0.329.0 → 0.330.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/boot/apply-boot-seeds.d.ts +8 -0
- package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
- package/dist/boot/boot-crypto.d.ts +9 -0
- package/dist/boot/boot-crypto.js +39 -0
- package/dist/boot/job-run-logger.d.ts +19 -0
- package/dist/boot/job-run-logger.js +44 -0
- package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
- package/dist/boot/pii-event-backfill-on-boot.js +25 -0
- package/dist/build-prod-bundle.d.ts +103 -0
- package/dist/build-prod-bundle.js +751 -0
- package/dist/bun-serve-options.d.ts +32 -0
- package/dist/bun-serve-options.js +49 -0
- package/dist/compose-features.d.ts +31 -0
- package/dist/compose-features.js +124 -0
- package/dist/extra-routes-deps.d.ts +30 -0
- package/dist/extra-routes-deps.js +4 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/pii-boot-gate.d.ts +13 -0
- package/dist/pii-boot-gate.js +46 -0
- package/dist/renderer-web-fonts.d.ts +9 -0
- package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
- package/dist/resolve-tailwind-cli.d.ts +14 -0
- package/dist/resolve-tailwind-cli.js +36 -0
- package/dist/run-prod-app-boot-context.d.ts +43 -0
- package/dist/run-prod-app-boot-context.js +198 -0
- package/dist/run-prod-app-static-files.d.ts +32 -0
- package/dist/run-prod-app-static-files.js +280 -0
- package/dist/run-prod-app.d.ts +471 -0
- package/dist/run-prod-app.js +741 -0
- package/dist/run-worker-app.d.ts +92 -0
- package/dist/run-worker-app.js +220 -0
- package/dist/security-headers.d.ts +20 -0
- package/dist/security-headers.js +49 -0
- package/dist/session-boot-gate.d.ts +11 -0
- package/dist/session-boot-gate.js +22 -0
- package/dist/session-wiring.d.ts +11 -0
- package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
- package/dist/try-hono-first.d.ts +24 -0
- package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
- package/package.json +102 -36
- package/src/changes.json +6 -0
- package/src/__tests__/boot-extra-context.test.ts +0 -163
- package/src/__tests__/boot-probe-fixture.ts +0 -64
- package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
- package/src/__tests__/build-prod-bundle.test.ts +0 -724
- 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 -609
- package/src/__tests__/compose-features.test.ts +0 -206
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/pii-boot-gate.test.ts +0 -112
- 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__/renderer-web-theme-plugin.integration.test.ts +0 -77
- package/src/__tests__/require-env.test.ts +0 -34
- package/src/__tests__/resolve-auth-mail.test.ts +0 -144
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
- package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
- package/src/__tests__/run-prod-app-spec.test.ts +0 -167
- package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
- package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
- package/src/__tests__/run-worker-app-boot.test.ts +0 -183
- package/src/__tests__/run-worker-app.integration.test.ts +0 -218
- package/src/__tests__/security-headers.test.ts +0 -88
- package/src/__tests__/session-boot-gate.test.ts +0 -70
- package/src/__tests__/session-wiring.test.ts +0 -16
- package/src/__tests__/try-hono-first.test.ts +0 -108
- package/src/__tests__/websocket-route.integration.test.ts +0 -695
- package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
- package/src/boot/__tests__/job-run-logger.test.ts +0 -26
- package/src/boot/boot-crypto.ts +0 -67
- package/src/boot/job-run-logger.ts +0 -63
- package/src/boot/pii-event-backfill-on-boot.ts +0 -43
- package/src/build-prod-bundle.ts +0 -996
- package/src/bun-serve-options.ts +0 -110
- package/src/compose-features.ts +0 -197
- package/src/extra-routes-deps.ts +0 -38
- package/src/index.ts +0 -27
- package/src/pii-boot-gate.ts +0 -80
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app-boot-context.ts +0 -319
- package/src/run-prod-app-static-files.ts +0 -359
- package/src/run-prod-app.ts +0 -1420
- package/src/run-worker-app.ts +0 -366
- package/src/security-headers.ts +0 -67
- package/src/session-boot-gate.ts +0 -34
package/src/bun-serve-options.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createKumikoWebSocketHandler,
|
|
3
|
-
DEFAULT_MAX_REQUEST_BYTES,
|
|
4
|
-
isWebSocketUpgradeRequest,
|
|
5
|
-
type KumikoServeEnv,
|
|
6
|
-
type KumikoWebSocketData,
|
|
7
|
-
WEBSOCKET_BACKPRESSURE_LIMIT_BYTES,
|
|
8
|
-
WEBSOCKET_MAX_PAYLOAD_BYTES,
|
|
9
|
-
WEBSOCKET_ROUTE_PATH_PREFIX,
|
|
10
|
-
} from "@cosmicdrift/kumiko-framework/api";
|
|
11
|
-
import type { Registry } from "@cosmicdrift/kumiko-framework/engine/types";
|
|
12
|
-
import {
|
|
13
|
-
readFilesRouteOptions,
|
|
14
|
-
resolveMaxUploadBodyBytes,
|
|
15
|
-
} from "@cosmicdrift/kumiko-framework/files";
|
|
16
|
-
|
|
17
|
-
// Fallback for callers that skip resolveDerivedMaxRequestBodySize below —
|
|
18
|
-
// Bun's own default is 128 MiB, buffered per request before any app-level
|
|
19
|
-
// body-limit middleware runs.
|
|
20
|
-
export const DEFAULT_MAX_REQUEST_BODY_SIZE_BYTES = 32 * 1024 * 1024;
|
|
21
|
-
|
|
22
|
-
// Keeps Bun's cap above the files route's own derived bodyLimit, so that
|
|
23
|
-
// route's JSON 413 fires before Bun's bare rejection would.
|
|
24
|
-
const BUN_BODY_LIMIT_SAFETY_MARGIN_BYTES = 8 * 1024;
|
|
25
|
-
|
|
26
|
-
// Derives Bun's request-body ceiling from the same registry-configured
|
|
27
|
-
// maxUploadSize/field maxSize the upload route itself enforces.
|
|
28
|
-
export function resolveDerivedMaxRequestBodySize(registry: Registry): number {
|
|
29
|
-
const filesRouteOptions = readFilesRouteOptions(registry);
|
|
30
|
-
return (
|
|
31
|
-
Math.max(
|
|
32
|
-
DEFAULT_MAX_REQUEST_BYTES,
|
|
33
|
-
resolveMaxUploadBodyBytes({ registry, maxUploadSize: filesRouteOptions.maxUploadSize }),
|
|
34
|
-
) + BUN_BODY_LIMIT_SAFETY_MARGIN_BYTES
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Other upgrades (dev HMR, ...) keep going through fetchHandler.
|
|
39
|
-
function isKumikoWebSocketUpgrade(req: Request): boolean {
|
|
40
|
-
return (
|
|
41
|
-
isWebSocketUpgradeRequest(req) &&
|
|
42
|
-
new URL(req.url).pathname.startsWith(WEBSOCKET_ROUTE_PATH_PREFIX)
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Bun.serve options for production.
|
|
48
|
-
*
|
|
49
|
-
* idleTimeout: 0 (disabled): SSE streams stay alive through their own
|
|
50
|
-
* heartbeat (SSE_HEARTBEAT_INTERVAL_MS in framework/api/sse-route.ts). With
|
|
51
|
-
* Bun's 10 s default, every heartbeat gap killed the connection with a half
|
|
52
|
-
* HTTP/2 RST_STREAM, which browsers report as ERR_HTTP2_PROTOCOL_ERROR.
|
|
53
|
-
*
|
|
54
|
-
* __tests__/run-prod-app-spec.test.ts pins the 0 against "looks like a
|
|
55
|
-
* leak" reverts.
|
|
56
|
-
*
|
|
57
|
-
* WebSocket: upgrade requests are branched off here, before `fetchHandler`
|
|
58
|
-
* (whose static/SPA layers clone the request, which `server.upgrade` can't
|
|
59
|
-
* use), into `webSocketOptions.upgradeFetch` with the original Request.
|
|
60
|
-
* `heartbeatIntervalMs` overrides the 25 s ping/revalidation cadence.
|
|
61
|
-
*/
|
|
62
|
-
export type WebSocketServeOptions = {
|
|
63
|
-
readonly upgradeFetch: (req: Request, env: KumikoServeEnv) => Response | Promise<Response>;
|
|
64
|
-
readonly heartbeatIntervalMs?: number;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export function buildBunServeOptions(
|
|
68
|
-
port: number,
|
|
69
|
-
fetchHandler: (req: Request, socketAddress?: string) => Response | Promise<Response>,
|
|
70
|
-
maxRequestBodySize: number = DEFAULT_MAX_REQUEST_BODY_SIZE_BYTES,
|
|
71
|
-
webSocketOptions?: WebSocketServeOptions,
|
|
72
|
-
): {
|
|
73
|
-
readonly port: number;
|
|
74
|
-
readonly fetch: (
|
|
75
|
-
req: Request,
|
|
76
|
-
server: Bun.Server<KumikoWebSocketData>,
|
|
77
|
-
) => Response | Promise<Response>;
|
|
78
|
-
readonly idleTimeout: number;
|
|
79
|
-
readonly maxRequestBodySize: number;
|
|
80
|
-
readonly websocket: Bun.WebSocketHandler<KumikoWebSocketData>;
|
|
81
|
-
} {
|
|
82
|
-
// `server.requestIP(req)` only resolves for the exact Request instance
|
|
83
|
-
// Bun created — extracted here, once, before any downstream req.clone()
|
|
84
|
-
// (tryHonoFirst et al.) can invalidate it.
|
|
85
|
-
return {
|
|
86
|
-
port,
|
|
87
|
-
fetch: (req, server) => {
|
|
88
|
-
const socketAddress = server.requestIP(req)?.address;
|
|
89
|
-
if (webSocketOptions && isKumikoWebSocketUpgrade(req)) {
|
|
90
|
-
return webSocketOptions.upgradeFetch(req, {
|
|
91
|
-
...(socketAddress !== undefined ? { socketAddress } : {}),
|
|
92
|
-
server,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return fetchHandler(req, socketAddress);
|
|
96
|
-
},
|
|
97
|
-
idleTimeout: 0,
|
|
98
|
-
maxRequestBodySize,
|
|
99
|
-
websocket: {
|
|
100
|
-
...createKumikoWebSocketHandler(
|
|
101
|
-
webSocketOptions?.heartbeatIntervalMs !== undefined
|
|
102
|
-
? { heartbeatIntervalMs: webSocketOptions.heartbeatIntervalMs }
|
|
103
|
-
: {},
|
|
104
|
-
),
|
|
105
|
-
maxPayloadLength: WEBSOCKET_MAX_PAYLOAD_BYTES,
|
|
106
|
-
backpressureLimit: WEBSOCKET_BACKPRESSURE_LIMIT_BYTES,
|
|
107
|
-
closeOnBackpressureLimit: true,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
package/src/compose-features.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
// composeFeatures — single source of truth for the feature list that
|
|
2
|
-
// boot AND the schema generator see.
|
|
3
|
-
//
|
|
4
|
-
// Both runDevApp and runProdApp mix in the same bundled features in
|
|
5
|
-
// auth-mode (config + user + tenant + auth-email-password, plus
|
|
6
|
-
// auth-self-registration when authOptions.signup is set). So the
|
|
7
|
-
// drizzle schema generator sees the exact same feature list per app as
|
|
8
|
-
// the runtime, the composition lives here — both bootstrap wrappers AND
|
|
9
|
-
// each app's drizzle/generate.ts call it.
|
|
10
|
-
//
|
|
11
|
-
// Order: infrastructure features (config/user/tenant) first, then
|
|
12
|
-
// auth-email-password (+ auth-self-registration when authOptions.signup
|
|
13
|
-
// is set), then the app features. Later features may reference earlier
|
|
14
|
-
// ones (e.g. authClaims hooks on user/tenant).
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
type AccountLockoutOptions,
|
|
18
|
-
type AccountUnlockOptions,
|
|
19
|
-
type AuthEmailPasswordOptions,
|
|
20
|
-
type AuthMailLocale,
|
|
21
|
-
createAuthEmailPasswordFeature,
|
|
22
|
-
createAuthSelfRegistrationToggleFeature,
|
|
23
|
-
type EmailVerificationOptions,
|
|
24
|
-
type InviteOptions,
|
|
25
|
-
type PasswordResetOptions,
|
|
26
|
-
type SignupOptions,
|
|
27
|
-
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
28
|
-
import {
|
|
29
|
-
AUTH_MFA_FEATURE,
|
|
30
|
-
mfaStatusCheckerFromFeature,
|
|
31
|
-
} from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
32
|
-
import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
33
|
-
import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
34
|
-
import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
35
|
-
import { dedupeFeatures, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
36
|
-
|
|
37
|
-
export type ComposeFeaturesOptions = {
|
|
38
|
-
/** When true, prepends config + user + tenant + auth-email-password
|
|
39
|
-
* (+ auth-self-registration when authOptions.signup is set) before the
|
|
40
|
-
* app features. Mirror of "auth-mode" in run{Dev,Prod}App. */
|
|
41
|
-
readonly includeBundled: boolean;
|
|
42
|
-
/** Optional auth-feature options passed through to
|
|
43
|
-
* createAuthEmailPasswordFeature. When passwordReset / emailVerification
|
|
44
|
-
* are set here, the feature registers the request/confirm handlers —
|
|
45
|
-
* otherwise it doesn't (500 if the routes are mounted via
|
|
46
|
-
* auth-routes.ts but no handler dispatches). Goes hand-in-hand with the
|
|
47
|
-
* passwordReset block in RunProdAppAuthOptions / RunDevAppAuthOptions. */
|
|
48
|
-
readonly authOptions?: AuthEmailPasswordOptions;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export function composeFeatures(
|
|
52
|
-
appFeatures: readonly FeatureDefinition[],
|
|
53
|
-
options: ComposeFeaturesOptions,
|
|
54
|
-
): FeatureDefinition[] {
|
|
55
|
-
// ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
|
|
56
|
-
// an app composing its own foundation and mounting auth-mfa itself is
|
|
57
|
-
// responsible for threading mfaStatusCheckerFromFeature(...) into its
|
|
58
|
-
// own createAuthEmailPasswordFeature(...) call, or login silently
|
|
59
|
-
// bypasses MFA. Upgrade if this trips someone: warn here when appFeatures
|
|
60
|
-
// contains AUTH_MFA_FEATURE but includeBundled is false.
|
|
61
|
-
if (!options.includeBundled) return dedupeFeatures(appFeatures);
|
|
62
|
-
|
|
63
|
-
// Bundled foundation goes first so its instances carry the runDevApp /
|
|
64
|
-
// runProdApp `authOptions` (passwordReset wiring etc.). App-features that
|
|
65
|
-
// ALSO declare one of these names — e.g. the create-kumiko-app picker
|
|
66
|
-
// hands back `createAuthEmailPasswordFeature()` because the user ticked
|
|
67
|
-
// it — would otherwise crash createRegistry with "Duplicate feature".
|
|
68
|
-
// Drop the app-side duplicates and warn so the user can clean run-config.
|
|
69
|
-
//
|
|
70
|
-
// auth-mfa is NOT part of the bundled foundation (apps opt in explicitly
|
|
71
|
-
// via APP_FEATURES) — but if it's there, the login handler needs its
|
|
72
|
-
// status-checker wired in at construction time, since createAuthEmail-
|
|
73
|
-
// PasswordFeature is built right here, before the caller ever sees it.
|
|
74
|
-
const dedupedAppFeatures = dedupeFeatures(appFeatures);
|
|
75
|
-
const mfaFeature = dedupedAppFeatures.find((f) => f.name === AUTH_MFA_FEATURE);
|
|
76
|
-
const authOptions = mfaFeature
|
|
77
|
-
? { ...options.authOptions, mfaStatusChecker: mfaStatusCheckerFromFeature(mfaFeature) }
|
|
78
|
-
: options.authOptions;
|
|
79
|
-
const bundled = [
|
|
80
|
-
createConfigFeature(),
|
|
81
|
-
createUserFeature(),
|
|
82
|
-
// inviteScreen mirrors authOptions.invite: that's the exact option that
|
|
83
|
-
// makes auth-email-password register the invite-create write-handler
|
|
84
|
-
// the /members invite drawer is bound to — without this, includeBundled
|
|
85
|
-
// apps that DO configure invite still get /members with no invite button.
|
|
86
|
-
createTenantFeature({ inviteScreen: Boolean(authOptions?.invite) }),
|
|
87
|
-
createAuthEmailPasswordFeature(authOptions ?? {}),
|
|
88
|
-
// signup-request/signup-confirm are registered whenever authOptions.signup
|
|
89
|
-
// is set (see above), but the handler itself no-ops unless the companion
|
|
90
|
-
// toggle feature is mounted (ctx.hasFeature(AUTH_SELF_REGISTRATION_FEATURE))
|
|
91
|
-
// — without this, apps using the includeBundled convenience path get
|
|
92
|
-
// self-signup silently broken (always-200 anti-enumeration contract masks
|
|
93
|
-
// it as success). Mount it alongside signup, default ON, matching the
|
|
94
|
-
// "on unless an operator flips it off at runtime" contract.
|
|
95
|
-
...(authOptions?.signup !== undefined ? [createAuthSelfRegistrationToggleFeature()] : []),
|
|
96
|
-
];
|
|
97
|
-
const bundledNames = new Set(bundled.map((f) => f.name));
|
|
98
|
-
const filteredApp: FeatureDefinition[] = [];
|
|
99
|
-
for (const f of dedupedAppFeatures) {
|
|
100
|
-
if (bundledNames.has(f.name)) {
|
|
101
|
-
// biome-ignore lint/suspicious/noConsole: boot-time UX warning
|
|
102
|
-
console.warn(
|
|
103
|
-
`[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.`,
|
|
104
|
-
);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
filteredApp.push(f);
|
|
108
|
-
}
|
|
109
|
-
return [...bundled, ...filteredApp];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Shape of any run{Prod,Dev}App auth block that can carry a
|
|
113
|
-
* passwordReset/emailVerification config, once resolveAuthMail has resolved
|
|
114
|
-
* the wrapper's optional hmacSecret into the feature API's required one —
|
|
115
|
-
* a structural-typed lookup on the auth-only subset is enough. Lets
|
|
116
|
-
* buildComposeAuthOptions be called with both RunProd- and
|
|
117
|
-
* RunDev-AuthOptions without building the helper twice. */
|
|
118
|
-
export type AuthOptionsCarrier = {
|
|
119
|
-
readonly passwordReset?: PasswordResetOptions;
|
|
120
|
-
readonly emailVerification?: EmailVerificationOptions;
|
|
121
|
-
readonly signup?: SignupOptions;
|
|
122
|
-
readonly invite?: InviteOptions;
|
|
123
|
-
readonly accountUnlock?: AccountUnlockOptions;
|
|
124
|
-
readonly accountLockout?: AccountLockoutOptions;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/** Builds the authOptions block for composeFeatures from a wrapper auth
|
|
128
|
-
* block. Passes through ONLY the feature-side fields (hmacSecret,
|
|
129
|
-
* tokenTtlMinutes, mode) — the mail side (sendResetEmail/appResetUrl)
|
|
130
|
-
* belongs in the auth-routes config and is wired separately by the
|
|
131
|
-
* wrapper.
|
|
132
|
-
*
|
|
133
|
-
* Returns undefined when neither passwordReset nor emailVerification is
|
|
134
|
-
* set (composeFeatures default-deny: NO handler registered in the
|
|
135
|
-
* registry, /api/auth/request-password-reset etc. stay 401/404). */
|
|
136
|
-
// All five magic-link flows (reset/verify/signup/invite/unlock) accept the
|
|
137
|
-
// same appUrl shape — a plain string, or a `(locale) => string` function for
|
|
138
|
-
// apps with language-in-path routing (see SignupRequestOptions.appUrl).
|
|
139
|
-
type MailFlowFields = {
|
|
140
|
-
readonly appUrl: string | ((locale: string) => string);
|
|
141
|
-
readonly tokenTtlMinutes?: number;
|
|
142
|
-
readonly appName?: string;
|
|
143
|
-
readonly locale?: AuthMailLocale;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// The magic-link mail fields shared by all five flows. Conditional spreads omit
|
|
147
|
-
// undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
|
|
148
|
-
// trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
|
|
149
|
-
// reset/verify layer hmacSecret (+ mode) on top.
|
|
150
|
-
function pickMailFields(src: MailFlowFields): MailFlowFields {
|
|
151
|
-
return {
|
|
152
|
-
appUrl: src.appUrl,
|
|
153
|
-
...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
|
|
154
|
-
...(src.appName !== undefined && { appName: src.appName }),
|
|
155
|
-
...(src.locale !== undefined && { locale: src.locale }),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function buildComposeAuthOptions(
|
|
160
|
-
auth: AuthOptionsCarrier | undefined,
|
|
161
|
-
): AuthEmailPasswordOptions | undefined {
|
|
162
|
-
if (!auth) return undefined;
|
|
163
|
-
const opts: { -readonly [K in keyof AuthEmailPasswordOptions]: AuthEmailPasswordOptions[K] } = {};
|
|
164
|
-
if (auth.passwordReset) {
|
|
165
|
-
opts.passwordReset = {
|
|
166
|
-
hmacSecret: auth.passwordReset.hmacSecret,
|
|
167
|
-
...pickMailFields(auth.passwordReset),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
if (auth.emailVerification) {
|
|
171
|
-
opts.emailVerification = {
|
|
172
|
-
hmacSecret: auth.emailVerification.hmacSecret,
|
|
173
|
-
...pickMailFields(auth.emailVerification),
|
|
174
|
-
...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
if (auth.signup) {
|
|
178
|
-
opts.signup = pickMailFields(auth.signup);
|
|
179
|
-
}
|
|
180
|
-
if (auth.invite) {
|
|
181
|
-
opts.invite = pickMailFields(auth.invite);
|
|
182
|
-
}
|
|
183
|
-
if (auth.accountUnlock) {
|
|
184
|
-
opts.accountUnlock = {
|
|
185
|
-
hmacSecret: auth.accountUnlock.hmacSecret,
|
|
186
|
-
...pickMailFields(auth.accountUnlock),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
if (auth.accountLockout) {
|
|
190
|
-
opts.accountLockout = auth.accountLockout;
|
|
191
|
-
}
|
|
192
|
-
return hasAnyAuthFlow(opts) ? opts : undefined;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function hasAnyAuthFlow(opts: AuthEmailPasswordOptions): boolean {
|
|
196
|
-
return Object.values(opts).some(Boolean);
|
|
197
|
-
}
|
package/src/extra-routes-deps.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import type { Registry, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import type Redis from "ioredis";
|
|
4
|
-
|
|
5
|
-
export type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
|
|
6
|
-
// Re-export the SystemAdmin write/query builders — the single source of
|
|
7
|
-
// truth lives in framework/api next to buildServer's own `extraRoutes`
|
|
8
|
-
// mount so the two never drift on how the SystemAdmin identity is built.
|
|
9
|
-
export {
|
|
10
|
-
makeDispatchSystemQuery,
|
|
11
|
-
makeDispatchSystemWrite,
|
|
12
|
-
} from "@cosmicdrift/kumiko-framework/api";
|
|
13
|
-
|
|
14
|
-
/** Deps for the `wire` hook (runProdApp/createKumikoServer, after
|
|
15
|
-
* buildServer has mounted `extraRoutes` — see kumiko-framework#3050) and
|
|
16
|
-
* for `runWorkerApp.wireComponents`. Naming: `deps` not `ctx` because
|
|
17
|
-
* `ctx` is the framework's HandlerContext (user/tenant/registry) — this
|
|
18
|
-
* scope is deliberately smaller and has NO `app` (routes are declared via
|
|
19
|
-
* `extraRoutes`, not wired here). */
|
|
20
|
-
export type SystemWireDeps = {
|
|
21
|
-
readonly db: DbConnection;
|
|
22
|
-
readonly redis: Redis;
|
|
23
|
-
/** Feature-registry — e.g. for plugin-lookups via
|
|
24
|
-
* `registry.getExtensionUsages("subscriptionProvider")`. */
|
|
25
|
-
readonly registry: Registry;
|
|
26
|
-
/** Writes through the /api/*-command-dispatcher (same idempotency/job
|
|
27
|
-
* hooks) as an auto-constructed SystemAdmin of the target tenant,
|
|
28
|
-
* WITHOUT the route's own access-check. Privilege-scope: SystemAdmin is
|
|
29
|
-
* the highest non-tenant-scoped role — reaches ANY SystemAdmin-gated
|
|
30
|
-
* handler on ANY tenant; the role-set is not configurable. Only for
|
|
31
|
-
* co-running components that already proved their own authenticity
|
|
32
|
-
* (e.g. an IMAP supervisor authenticated against its own mailbox). */
|
|
33
|
-
readonly dispatchSystemWrite: (args: {
|
|
34
|
-
readonly handlerQn: string;
|
|
35
|
-
readonly payload: unknown;
|
|
36
|
-
readonly tenantId: import("@cosmicdrift/kumiko-framework/engine").TenantId;
|
|
37
|
-
}) => Promise<WriteResult>;
|
|
38
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// Public API für den Kumiko-Production-Server-Boot. Symmetrisch zu
|
|
2
|
-
// runDevApp (kumiko-dev-server), aber ohne Dev-/Scaffold-/Codegen-
|
|
3
|
-
// Tooling (ts-morph) als Dependency — Prod-Apps ziehen so kein
|
|
4
|
-
// Dev-Tooling mehr in ihre node_modules.
|
|
5
|
-
export { type ComposeFeaturesOptions, composeFeatures } from "./compose-features";
|
|
6
|
-
export type {
|
|
7
|
-
AccountUnlockSetup,
|
|
8
|
-
EmailVerificationSetup,
|
|
9
|
-
InviteSetup,
|
|
10
|
-
PageHeadMeta,
|
|
11
|
-
PageHeadResolver,
|
|
12
|
-
PageHeadSystemQuery,
|
|
13
|
-
PasswordResetSetup,
|
|
14
|
-
ProdAppHandle,
|
|
15
|
-
ProdSeedFn,
|
|
16
|
-
RunProdAppAuthOptions,
|
|
17
|
-
RunProdAppOptions,
|
|
18
|
-
SignupSetup,
|
|
19
|
-
} from "./run-prod-app";
|
|
20
|
-
export { requireEnv, runProdApp } from "./run-prod-app";
|
|
21
|
-
export type {
|
|
22
|
-
RunWorkerAppOptions,
|
|
23
|
-
WorkerAppHandle,
|
|
24
|
-
WorkerWireDeps,
|
|
25
|
-
} from "./run-worker-app";
|
|
26
|
-
export { runWorkerApp } from "./run-worker-app";
|
|
27
|
-
export type { SecurityHeadersOption } from "./security-headers";
|
package/src/pii-boot-gate.ts
DELETED
|
@@ -1,80 +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
|
-
// fw#2776: a declared event stance is worth as much as an entity annotation —
|
|
43
|
-
// encryptEventPayloadPii no-ops without a KMS, so a catalogued event writes
|
|
44
|
-
// plaintext PII into kumiko_events with nothing to crypto-shred later.
|
|
45
|
-
const piiEvents = features.flatMap((feature) =>
|
|
46
|
-
Object.values(feature.events ?? {})
|
|
47
|
-
.filter((event) => event.piiFields !== undefined && event.piiFields !== "none")
|
|
48
|
-
.map((event) => event.name),
|
|
49
|
-
);
|
|
50
|
-
// skip: nothing PII-annotated is mounted — plaintext gate is moot.
|
|
51
|
-
if (piiEntities.length === 0 && piiEvents.length === 0) return;
|
|
52
|
-
const carriers = [
|
|
53
|
-
...(piiEntities.length > 0 ? [`${piiEntities.length} entities`] : []),
|
|
54
|
-
...(piiEvents.length > 0 ? [`${piiEvents.length} events`] : []),
|
|
55
|
-
].join(" and ");
|
|
56
|
-
const named = [
|
|
57
|
-
...(piiEntities.length > 0 ? [`entities [${piiEntities.join(", ")}]`] : []),
|
|
58
|
-
...(piiEvents.length > 0 ? [`events [${piiEvents.join(", ")}]`] : []),
|
|
59
|
-
].join(" and ");
|
|
60
|
-
|
|
61
|
-
if (opts.mode === "dev") {
|
|
62
|
-
// biome-ignore lint/suspicious/noConsole: boot-time security warning
|
|
63
|
-
console.warn(
|
|
64
|
-
`[${tag}] ${carriers} carry pii/userOwned/tenantOwned/recordOwned annotations or a non-"none" defineEvent PII stance 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.`,
|
|
65
|
-
);
|
|
66
|
-
// skip: dev mode, plaintext-PII warning already logged above
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (opts.allowPlaintextPii) {
|
|
70
|
-
// biome-ignore lint/suspicious/noConsole: boot-time security warning
|
|
71
|
-
console.warn(
|
|
72
|
-
`[${tag}] ${carriers} 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.`,
|
|
73
|
-
);
|
|
74
|
-
// skip: operator explicitly acknowledged plaintext PII via allowPlaintextPii, warning already logged above
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
throw new Error(
|
|
78
|
-
`[${tag}] BOOT ABORTED — ${named} carry pii/userOwned/tenantOwned/recordOwned annotations or a non-"none" defineEvent PII stance 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.`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
@@ -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
|
-
}
|