@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.
Files changed (90) hide show
  1. package/README.md +15 -0
  2. package/dist/boot/apply-boot-seeds.d.ts +8 -0
  3. package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
  4. package/dist/boot/boot-crypto.d.ts +9 -0
  5. package/dist/boot/boot-crypto.js +39 -0
  6. package/dist/boot/job-run-logger.d.ts +19 -0
  7. package/dist/boot/job-run-logger.js +44 -0
  8. package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
  9. package/dist/boot/pii-event-backfill-on-boot.js +25 -0
  10. package/dist/build-prod-bundle.d.ts +103 -0
  11. package/dist/build-prod-bundle.js +751 -0
  12. package/dist/bun-serve-options.d.ts +32 -0
  13. package/dist/bun-serve-options.js +49 -0
  14. package/dist/compose-features.d.ts +31 -0
  15. package/dist/compose-features.js +124 -0
  16. package/dist/extra-routes-deps.d.ts +30 -0
  17. package/dist/extra-routes-deps.js +4 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +7 -0
  20. package/dist/pii-boot-gate.d.ts +13 -0
  21. package/dist/pii-boot-gate.js +46 -0
  22. package/dist/renderer-web-fonts.d.ts +9 -0
  23. package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
  24. package/dist/resolve-tailwind-cli.d.ts +14 -0
  25. package/dist/resolve-tailwind-cli.js +36 -0
  26. package/dist/run-prod-app-boot-context.d.ts +43 -0
  27. package/dist/run-prod-app-boot-context.js +198 -0
  28. package/dist/run-prod-app-static-files.d.ts +32 -0
  29. package/dist/run-prod-app-static-files.js +280 -0
  30. package/dist/run-prod-app.d.ts +471 -0
  31. package/dist/run-prod-app.js +741 -0
  32. package/dist/run-worker-app.d.ts +92 -0
  33. package/dist/run-worker-app.js +220 -0
  34. package/dist/security-headers.d.ts +20 -0
  35. package/dist/security-headers.js +49 -0
  36. package/dist/session-boot-gate.d.ts +11 -0
  37. package/dist/session-boot-gate.js +22 -0
  38. package/dist/session-wiring.d.ts +11 -0
  39. package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
  40. package/dist/try-hono-first.d.ts +24 -0
  41. package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
  42. package/package.json +102 -36
  43. package/src/changes.json +6 -0
  44. package/src/__tests__/boot-extra-context.test.ts +0 -163
  45. package/src/__tests__/boot-probe-fixture.ts +0 -64
  46. package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
  47. package/src/__tests__/build-prod-bundle.test.ts +0 -724
  48. package/src/__tests__/cache-headers.test.ts +0 -83
  49. package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
  50. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -609
  51. package/src/__tests__/compose-features.test.ts +0 -206
  52. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  53. package/src/__tests__/pii-boot-gate.test.ts +0 -112
  54. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  55. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  56. package/src/__tests__/renderer-web-theme-plugin.integration.test.ts +0 -77
  57. package/src/__tests__/require-env.test.ts +0 -34
  58. package/src/__tests__/resolve-auth-mail.test.ts +0 -144
  59. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  60. package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
  61. package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
  62. package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
  63. package/src/__tests__/run-prod-app-spec.test.ts +0 -167
  64. package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
  65. package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
  66. package/src/__tests__/run-worker-app-boot.test.ts +0 -183
  67. package/src/__tests__/run-worker-app.integration.test.ts +0 -218
  68. package/src/__tests__/security-headers.test.ts +0 -88
  69. package/src/__tests__/session-boot-gate.test.ts +0 -70
  70. package/src/__tests__/session-wiring.test.ts +0 -16
  71. package/src/__tests__/try-hono-first.test.ts +0 -108
  72. package/src/__tests__/websocket-route.integration.test.ts +0 -695
  73. package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
  74. package/src/boot/__tests__/job-run-logger.test.ts +0 -26
  75. package/src/boot/boot-crypto.ts +0 -67
  76. package/src/boot/job-run-logger.ts +0 -63
  77. package/src/boot/pii-event-backfill-on-boot.ts +0 -43
  78. package/src/build-prod-bundle.ts +0 -996
  79. package/src/bun-serve-options.ts +0 -110
  80. package/src/compose-features.ts +0 -197
  81. package/src/extra-routes-deps.ts +0 -38
  82. package/src/index.ts +0 -27
  83. package/src/pii-boot-gate.ts +0 -80
  84. package/src/resolve-tailwind-cli.ts +0 -45
  85. package/src/run-prod-app-boot-context.ts +0 -319
  86. package/src/run-prod-app-static-files.ts +0 -359
  87. package/src/run-prod-app.ts +0 -1420
  88. package/src/run-worker-app.ts +0 -366
  89. package/src/security-headers.ts +0 -67
  90. package/src/session-boot-gate.ts +0 -34
@@ -0,0 +1,32 @@
1
+ /// <reference types="bun-types" preserve="true" />
2
+ import { type KumikoServeEnv, type KumikoWebSocketData } from "@cosmicdrift/kumiko-framework/api";
3
+ import type { Registry } from "@cosmicdrift/kumiko-framework/engine/types";
4
+ export declare const DEFAULT_MAX_REQUEST_BODY_SIZE_BYTES: number;
5
+ export declare function resolveDerivedMaxRequestBodySize(registry: Registry): number;
6
+ /**
7
+ * Bun.serve options for production.
8
+ *
9
+ * idleTimeout: 0 (disabled): SSE streams stay alive through their own
10
+ * heartbeat (SSE_HEARTBEAT_INTERVAL_MS in framework/api/sse-route.ts). With
11
+ * Bun's 10 s default, every heartbeat gap killed the connection with a half
12
+ * HTTP/2 RST_STREAM, which browsers report as ERR_HTTP2_PROTOCOL_ERROR.
13
+ *
14
+ * __tests__/run-prod-app-spec.test.ts pins the 0 against "looks like a
15
+ * leak" reverts.
16
+ *
17
+ * WebSocket: upgrade requests are branched off here, before `fetchHandler`
18
+ * (whose static/SPA layers clone the request, which `server.upgrade` can't
19
+ * use), into `webSocketOptions.upgradeFetch` with the original Request.
20
+ * `heartbeatIntervalMs` overrides the 25 s ping/revalidation cadence.
21
+ */
22
+ export type WebSocketServeOptions = {
23
+ readonly upgradeFetch: (req: Request, env: KumikoServeEnv) => Response | Promise<Response>;
24
+ readonly heartbeatIntervalMs?: number;
25
+ };
26
+ export declare function buildBunServeOptions(port: number, fetchHandler: (req: Request, socketAddress?: string) => Response | Promise<Response>, maxRequestBodySize?: number, webSocketOptions?: WebSocketServeOptions): {
27
+ readonly port: number;
28
+ readonly fetch: (req: Request, server: Bun.Server<KumikoWebSocketData>) => Response | Promise<Response>;
29
+ readonly idleTimeout: number;
30
+ readonly maxRequestBodySize: number;
31
+ readonly websocket: Bun.WebSocketHandler<KumikoWebSocketData>;
32
+ };
@@ -0,0 +1,49 @@
1
+ /// <reference types="bun-types" preserve="true" />
2
+ import { createKumikoWebSocketHandler, DEFAULT_MAX_REQUEST_BYTES, isWebSocketUpgradeRequest, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_ROUTE_PATH_PREFIX, } from "@cosmicdrift/kumiko-framework/api";
3
+ import { readFilesRouteOptions, resolveMaxUploadBodyBytes, } from "@cosmicdrift/kumiko-framework/files";
4
+ // Fallback for callers that skip resolveDerivedMaxRequestBodySize below —
5
+ // Bun's own default is 128 MiB, buffered per request before any app-level
6
+ // body-limit middleware runs.
7
+ export const DEFAULT_MAX_REQUEST_BODY_SIZE_BYTES = 32 * 1024 * 1024;
8
+ // Keeps Bun's cap above the files route's own derived bodyLimit, so that
9
+ // route's JSON 413 fires before Bun's bare rejection would.
10
+ const BUN_BODY_LIMIT_SAFETY_MARGIN_BYTES = 8 * 1024;
11
+ // Derives Bun's request-body ceiling from the same registry-configured
12
+ // maxUploadSize/field maxSize the upload route itself enforces.
13
+ export function resolveDerivedMaxRequestBodySize(registry) {
14
+ const filesRouteOptions = readFilesRouteOptions(registry);
15
+ return (Math.max(DEFAULT_MAX_REQUEST_BYTES, resolveMaxUploadBodyBytes({ registry, maxUploadSize: filesRouteOptions.maxUploadSize })) + BUN_BODY_LIMIT_SAFETY_MARGIN_BYTES);
16
+ }
17
+ // Other upgrades (dev HMR, ...) keep going through fetchHandler.
18
+ function isKumikoWebSocketUpgrade(req) {
19
+ return (isWebSocketUpgradeRequest(req) &&
20
+ new URL(req.url).pathname.startsWith(WEBSOCKET_ROUTE_PATH_PREFIX));
21
+ }
22
+ export function buildBunServeOptions(port, fetchHandler, maxRequestBodySize = DEFAULT_MAX_REQUEST_BODY_SIZE_BYTES, webSocketOptions) {
23
+ // `server.requestIP(req)` only resolves for the exact Request instance
24
+ // Bun created — extracted here, once, before any downstream req.clone()
25
+ // (tryHonoFirst et al.) can invalidate it.
26
+ return {
27
+ port,
28
+ fetch: (req, server) => {
29
+ const socketAddress = server.requestIP(req)?.address;
30
+ if (webSocketOptions && isKumikoWebSocketUpgrade(req)) {
31
+ return webSocketOptions.upgradeFetch(req, {
32
+ ...(socketAddress !== undefined ? { socketAddress } : {}),
33
+ server,
34
+ });
35
+ }
36
+ return fetchHandler(req, socketAddress);
37
+ },
38
+ idleTimeout: 0,
39
+ maxRequestBodySize,
40
+ websocket: {
41
+ ...createKumikoWebSocketHandler(webSocketOptions?.heartbeatIntervalMs !== undefined
42
+ ? { heartbeatIntervalMs: webSocketOptions.heartbeatIntervalMs }
43
+ : {}),
44
+ maxPayloadLength: WEBSOCKET_MAX_PAYLOAD_BYTES,
45
+ backpressureLimit: WEBSOCKET_BACKPRESSURE_LIMIT_BYTES,
46
+ closeOnBackpressureLimit: true,
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,31 @@
1
+ import { type AccountLockoutOptions, type AccountUnlockOptions, type AuthEmailPasswordOptions, type EmailVerificationOptions, type InviteOptions, type PasswordResetOptions, type SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
+ import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
3
+ export type ComposeFeaturesOptions = {
4
+ /** When true, prepends config + user + tenant + auth-email-password
5
+ * (+ auth-self-registration when authOptions.signup is set) before the
6
+ * app features. Mirror of "auth-mode" in run{Dev,Prod}App. */
7
+ readonly includeBundled: boolean;
8
+ /** Optional auth-feature options passed through to
9
+ * createAuthEmailPasswordFeature. When passwordReset / emailVerification
10
+ * are set here, the feature registers the request/confirm handlers —
11
+ * otherwise it doesn't (500 if the routes are mounted via
12
+ * auth-routes.ts but no handler dispatches). Goes hand-in-hand with the
13
+ * passwordReset block in RunProdAppAuthOptions / RunDevAppAuthOptions. */
14
+ readonly authOptions?: AuthEmailPasswordOptions;
15
+ };
16
+ export declare function composeFeatures(appFeatures: readonly FeatureDefinition[], options: ComposeFeaturesOptions): FeatureDefinition[];
17
+ /** Shape of any run{Prod,Dev}App auth block that can carry a
18
+ * passwordReset/emailVerification config, once resolveAuthMail has resolved
19
+ * the wrapper's optional hmacSecret into the feature API's required one —
20
+ * a structural-typed lookup on the auth-only subset is enough. Lets
21
+ * buildComposeAuthOptions be called with both RunProd- and
22
+ * RunDev-AuthOptions without building the helper twice. */
23
+ export type AuthOptionsCarrier = {
24
+ readonly passwordReset?: PasswordResetOptions;
25
+ readonly emailVerification?: EmailVerificationOptions;
26
+ readonly signup?: SignupOptions;
27
+ readonly invite?: InviteOptions;
28
+ readonly accountUnlock?: AccountUnlockOptions;
29
+ readonly accountLockout?: AccountLockoutOptions;
30
+ };
31
+ export declare function buildComposeAuthOptions(auth: AuthOptionsCarrier | undefined): AuthEmailPasswordOptions | undefined;
@@ -0,0 +1,124 @@
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
+ import { createAuthEmailPasswordFeature, createAuthSelfRegistrationToggleFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
16
+ import { AUTH_MFA_FEATURE, mfaStatusCheckerFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
17
+ import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
18
+ import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
19
+ import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
20
+ import { dedupeFeatures } from "@cosmicdrift/kumiko-framework/engine";
21
+ export function composeFeatures(appFeatures, options) {
22
+ // ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
23
+ // an app composing its own foundation and mounting auth-mfa itself is
24
+ // responsible for threading mfaStatusCheckerFromFeature(...) into its
25
+ // own createAuthEmailPasswordFeature(...) call, or login silently
26
+ // bypasses MFA. Upgrade if this trips someone: warn here when appFeatures
27
+ // contains AUTH_MFA_FEATURE but includeBundled is false.
28
+ if (!options.includeBundled)
29
+ return dedupeFeatures(appFeatures);
30
+ // Bundled foundation goes first so its instances carry the runDevApp /
31
+ // runProdApp `authOptions` (passwordReset wiring etc.). App-features that
32
+ // ALSO declare one of these names — e.g. the create-kumiko-app picker
33
+ // hands back `createAuthEmailPasswordFeature()` because the user ticked
34
+ // it — would otherwise crash createRegistry with "Duplicate feature".
35
+ // Drop the app-side duplicates and warn so the user can clean run-config.
36
+ //
37
+ // auth-mfa is NOT part of the bundled foundation (apps opt in explicitly
38
+ // via APP_FEATURES) — but if it's there, the login handler needs its
39
+ // status-checker wired in at construction time, since createAuthEmail-
40
+ // PasswordFeature is built right here, before the caller ever sees it.
41
+ const dedupedAppFeatures = dedupeFeatures(appFeatures);
42
+ const mfaFeature = dedupedAppFeatures.find((f) => f.name === AUTH_MFA_FEATURE);
43
+ const authOptions = mfaFeature
44
+ ? { ...options.authOptions, mfaStatusChecker: mfaStatusCheckerFromFeature(mfaFeature) }
45
+ : options.authOptions;
46
+ const bundled = [
47
+ createConfigFeature(),
48
+ createUserFeature(),
49
+ // inviteScreen mirrors authOptions.invite: that's the exact option that
50
+ // makes auth-email-password register the invite-create write-handler
51
+ // the /members invite drawer is bound to — without this, includeBundled
52
+ // apps that DO configure invite still get /members with no invite button.
53
+ createTenantFeature({ inviteScreen: Boolean(authOptions?.invite) }),
54
+ createAuthEmailPasswordFeature(authOptions ?? {}),
55
+ // signup-request/signup-confirm are registered whenever authOptions.signup
56
+ // is set (see above), but the handler itself no-ops unless the companion
57
+ // toggle feature is mounted (ctx.hasFeature(AUTH_SELF_REGISTRATION_FEATURE))
58
+ // — without this, apps using the includeBundled convenience path get
59
+ // self-signup silently broken (always-200 anti-enumeration contract masks
60
+ // it as success). Mount it alongside signup, default ON, matching the
61
+ // "on unless an operator flips it off at runtime" contract.
62
+ ...(authOptions?.signup !== undefined ? [createAuthSelfRegistrationToggleFeature()] : []),
63
+ ];
64
+ const bundledNames = new Set(bundled.map((f) => f.name));
65
+ const filteredApp = [];
66
+ for (const f of dedupedAppFeatures) {
67
+ if (bundledNames.has(f.name)) {
68
+ // biome-ignore lint/suspicious/noConsole: boot-time UX warning
69
+ console.warn(`[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.`);
70
+ continue;
71
+ }
72
+ filteredApp.push(f);
73
+ }
74
+ return [...bundled, ...filteredApp];
75
+ }
76
+ // The magic-link mail fields shared by all five flows. Conditional spreads omit
77
+ // undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
78
+ // trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
79
+ // reset/verify layer hmacSecret (+ mode) on top.
80
+ function pickMailFields(src) {
81
+ return {
82
+ appUrl: src.appUrl,
83
+ ...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
84
+ ...(src.appName !== undefined && { appName: src.appName }),
85
+ ...(src.locale !== undefined && { locale: src.locale }),
86
+ };
87
+ }
88
+ export function buildComposeAuthOptions(auth) {
89
+ if (!auth)
90
+ return undefined;
91
+ const opts = {};
92
+ if (auth.passwordReset) {
93
+ opts.passwordReset = {
94
+ hmacSecret: auth.passwordReset.hmacSecret,
95
+ ...pickMailFields(auth.passwordReset),
96
+ };
97
+ }
98
+ if (auth.emailVerification) {
99
+ opts.emailVerification = {
100
+ hmacSecret: auth.emailVerification.hmacSecret,
101
+ ...pickMailFields(auth.emailVerification),
102
+ ...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
103
+ };
104
+ }
105
+ if (auth.signup) {
106
+ opts.signup = pickMailFields(auth.signup);
107
+ }
108
+ if (auth.invite) {
109
+ opts.invite = pickMailFields(auth.invite);
110
+ }
111
+ if (auth.accountUnlock) {
112
+ opts.accountUnlock = {
113
+ hmacSecret: auth.accountUnlock.hmacSecret,
114
+ ...pickMailFields(auth.accountUnlock),
115
+ };
116
+ }
117
+ if (auth.accountLockout) {
118
+ opts.accountLockout = auth.accountLockout;
119
+ }
120
+ return hasAnyAuthFlow(opts) ? opts : undefined;
121
+ }
122
+ function hasAnyAuthFlow(opts) {
123
+ return Object.values(opts).some(Boolean);
124
+ }
@@ -0,0 +1,30 @@
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
+ export type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
5
+ export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
6
+ /** Deps for the `wire` hook (runProdApp/createKumikoServer, after
7
+ * buildServer has mounted `extraRoutes` — see kumiko-framework#3050) and
8
+ * for `runWorkerApp.wireComponents`. Naming: `deps` not `ctx` because
9
+ * `ctx` is the framework's HandlerContext (user/tenant/registry) — this
10
+ * scope is deliberately smaller and has NO `app` (routes are declared via
11
+ * `extraRoutes`, not wired here). */
12
+ export type SystemWireDeps = {
13
+ readonly db: DbConnection;
14
+ readonly redis: Redis;
15
+ /** Feature-registry — e.g. for plugin-lookups via
16
+ * `registry.getExtensionUsages("subscriptionProvider")`. */
17
+ readonly registry: Registry;
18
+ /** Writes through the /api/*-command-dispatcher (same idempotency/job
19
+ * hooks) as an auto-constructed SystemAdmin of the target tenant,
20
+ * WITHOUT the route's own access-check. Privilege-scope: SystemAdmin is
21
+ * the highest non-tenant-scoped role — reaches ANY SystemAdmin-gated
22
+ * handler on ANY tenant; the role-set is not configurable. Only for
23
+ * co-running components that already proved their own authenticity
24
+ * (e.g. an IMAP supervisor authenticated against its own mailbox). */
25
+ readonly dispatchSystemWrite: (args: {
26
+ readonly handlerQn: string;
27
+ readonly payload: unknown;
28
+ readonly tenantId: import("@cosmicdrift/kumiko-framework/engine").TenantId;
29
+ }) => Promise<WriteResult>;
30
+ };
@@ -0,0 +1,4 @@
1
+ // Re-export the SystemAdmin write/query builders — the single source of
2
+ // truth lives in framework/api next to buildServer's own `extraRoutes`
3
+ // mount so the two never drift on how the SystemAdmin identity is built.
4
+ export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
@@ -0,0 +1,6 @@
1
+ export { type ComposeFeaturesOptions, composeFeatures } from "./compose-features.js";
2
+ export type { AccountUnlockSetup, EmailVerificationSetup, InviteSetup, PageHeadMeta, PageHeadResolver, PageHeadSystemQuery, PasswordResetSetup, ProdAppHandle, ProdSeedFn, RunProdAppAuthOptions, RunProdAppOptions, SignupSetup, } from "./run-prod-app.js";
3
+ export { requireEnv, runProdApp } from "./run-prod-app.js";
4
+ export type { RunWorkerAppOptions, WorkerAppHandle, WorkerWireDeps, } from "./run-worker-app.js";
5
+ export { runWorkerApp } from "./run-worker-app.js";
6
+ export type { SecurityHeadersOption } from "./security-headers.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
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 { composeFeatures } from "./compose-features.js";
6
+ export { requireEnv, runProdApp } from "./run-prod-app.js";
7
+ export { runWorkerApp } from "./run-worker-app.js";
@@ -0,0 +1,13 @@
1
+ import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
2
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
3
+ type PiiGateOptions = {
4
+ readonly kms?: KmsAdapter | undefined;
5
+ readonly blindIndexKey?: string | undefined;
6
+ readonly allowPlaintextPii?: string | undefined;
7
+ /** prod fails hard on plaintext PII (opt-out via allowPlaintextPii);
8
+ * dev only warns — local data, and an InMemory KMS against a persistent
9
+ * dev DB would strand every row after a restart. */
10
+ readonly mode: "prod" | "dev";
11
+ };
12
+ export declare function assertPiiBootInvariants(features: readonly FeatureDefinition[], opts: PiiGateOptions): void;
13
+ export {};
@@ -0,0 +1,46 @@
1
+ import { collectLookupableFields, collectPiiSubjectFields, } from "@cosmicdrift/kumiko-framework/crypto";
2
+ export function assertPiiBootInvariants(features, opts) {
3
+ const tag = opts.mode === "prod" ? "runProdApp" : "runDevApp";
4
+ const lookupableEntities = features.flatMap((feature) => Object.entries(feature.entities ?? {})
5
+ .filter(([, entity]) => collectLookupableFields(entity).length > 0)
6
+ .map(([name]) => name));
7
+ if (opts.kms && !opts.blindIndexKey && lookupableEntities.length > 0) {
8
+ throw new Error(`[${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).`);
9
+ }
10
+ // skip: KMS configured — PII fields are encrypted, nothing left to gate.
11
+ if (opts.kms)
12
+ return;
13
+ const piiEntities = features.flatMap((feature) => Object.entries(feature.entities ?? {})
14
+ .filter(([, entity]) => collectPiiSubjectFields(entity).length > 0)
15
+ .map(([name]) => name));
16
+ // fw#2776: a declared event stance is worth as much as an entity annotation —
17
+ // encryptEventPayloadPii no-ops without a KMS, so a catalogued event writes
18
+ // plaintext PII into kumiko_events with nothing to crypto-shred later.
19
+ const piiEvents = features.flatMap((feature) => Object.values(feature.events ?? {})
20
+ .filter((event) => event.piiFields !== undefined && event.piiFields !== "none")
21
+ .map((event) => event.name));
22
+ // skip: nothing PII-annotated is mounted — plaintext gate is moot.
23
+ if (piiEntities.length === 0 && piiEvents.length === 0)
24
+ return;
25
+ const carriers = [
26
+ ...(piiEntities.length > 0 ? [`${piiEntities.length} entities`] : []),
27
+ ...(piiEvents.length > 0 ? [`${piiEvents.length} events`] : []),
28
+ ].join(" and ");
29
+ const named = [
30
+ ...(piiEntities.length > 0 ? [`entities [${piiEntities.join(", ")}]`] : []),
31
+ ...(piiEvents.length > 0 ? [`events [${piiEvents.join(", ")}]`] : []),
32
+ ].join(" and ");
33
+ if (opts.mode === "dev") {
34
+ // biome-ignore lint/suspicious/noConsole: boot-time security warning
35
+ console.warn(`[${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.`);
36
+ // skip: dev mode, plaintext-PII warning already logged above
37
+ return;
38
+ }
39
+ if (opts.allowPlaintextPii) {
40
+ // biome-ignore lint/suspicious/noConsole: boot-time security warning
41
+ console.warn(`[${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.`);
42
+ // skip: operator explicitly acknowledged plaintext PII via allowPlaintextPii, warning already logged above
43
+ return;
44
+ }
45
+ throw new Error(`[${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.`);
46
+ }
@@ -0,0 +1,9 @@
1
+ export declare const RENDERER_WEB_FONTS_URL_PREFIX = "/assets/kumiko/fonts/";
2
+ /** dist-relative folder the prod build copies the fonts into. */
3
+ export declare const RENDERER_WEB_FONTS_DIST_DIR = "assets/kumiko/fonts";
4
+ /** Allowlist for servable font file names; also rules out path segments. */
5
+ export declare const RENDERER_WEB_FONT_FILE_PATTERN: RegExp;
6
+ /** Directory of the woff2 files next to renderer-web's styles.css, or
7
+ * undefined when the package can't be resolved from any `searchFrom` dir
8
+ * (or outside Bun, which does the package-exports resolution). */
9
+ export declare function resolveRendererWebFontsDir(searchFrom: readonly string[]): string | undefined;
@@ -2,32 +2,28 @@
2
2
  // same-origin URL. Relative url() would 404: the CSS is compiled into a temp
3
3
  // dir (dev) or hashed into dist/assets (prod), away from the source fonts.
4
4
  // data: URIs are out because apps run with `font-src 'self'`.
5
-
6
5
  import { dirname, join } from "node:path";
7
-
8
6
  export const RENDERER_WEB_FONTS_URL_PREFIX = "/assets/kumiko/fonts/";
9
-
10
7
  /** dist-relative folder the prod build copies the fonts into. */
11
8
  export const RENDERER_WEB_FONTS_DIST_DIR = "assets/kumiko/fonts";
12
-
13
9
  /** Allowlist for servable font file names; also rules out path segments. */
14
10
  export const RENDERER_WEB_FONT_FILE_PATTERN = /^[a-z0-9-]+\.woff2$/;
15
-
16
- const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
17
-
11
+ const hasBun = typeof globalThis.Bun !== "undefined";
18
12
  /** Directory of the woff2 files next to renderer-web's styles.css, or
19
13
  * undefined when the package can't be resolved from any `searchFrom` dir
20
14
  * (or outside Bun, which does the package-exports resolution). */
21
- export function resolveRendererWebFontsDir(searchFrom: readonly string[]): string | undefined {
22
- if (!hasBun) return undefined;
23
- const bun = (globalThis as { Bun: { resolveSync: (id: string, from: string) => string } }).Bun;
24
- for (const from of searchFrom) {
25
- try {
26
- const stylesheet = bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", from);
27
- return join(dirname(stylesheet), "fonts");
28
- } catch {
29
- // try the next search root
15
+ export function resolveRendererWebFontsDir(searchFrom) {
16
+ if (!hasBun)
17
+ return undefined;
18
+ const bun = globalThis.Bun;
19
+ for (const from of searchFrom) {
20
+ try {
21
+ const stylesheet = bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", from);
22
+ return join(dirname(stylesheet), "fonts");
23
+ }
24
+ catch {
25
+ // try the next search root
26
+ }
30
27
  }
31
- }
32
- return undefined;
28
+ return undefined;
33
29
  }
@@ -0,0 +1,14 @@
1
+ type BunResolver = {
2
+ resolveSync: (id: string, from: string) => string;
3
+ };
4
+ export type ResolveTailwindCliDeps = {
5
+ readonly bun?: BunResolver;
6
+ readonly cwd: string;
7
+ };
8
+ export declare function resolveTailwindCli(deps: ResolveTailwindCliDeps): string | undefined;
9
+ /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
10
+ * CSS file. Skip renderer-web fallback (or fail build) when peer deps
11
+ * aren't resolvable from that directory — e.g. Bun cache copies of
12
+ * `@cosmicdrift/kumiko-renderer-web` outside a hoisted node_modules tree. */
13
+ export declare function canResolveTailwindStylesheet(entryCss: string, deps: ResolveTailwindCliDeps): boolean;
14
+ export {};
@@ -0,0 +1,36 @@
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
+ import { dirname, resolve } from "node:path";
10
+ export function resolveTailwindCli(deps) {
11
+ if (deps.bun === undefined)
12
+ return undefined;
13
+ try {
14
+ const pkgJsonPath = deps.bun.resolveSync("@tailwindcss/cli/package.json", deps.cwd);
15
+ // bin: { tailwindcss: "./dist/index.mjs" } → absoluter Pfad
16
+ return resolve(pkgJsonPath, "..", "dist", "index.mjs");
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
23
+ * CSS file. Skip renderer-web fallback (or fail build) when peer deps
24
+ * aren't resolvable from that directory — e.g. Bun cache copies of
25
+ * `@cosmicdrift/kumiko-renderer-web` outside a hoisted node_modules tree. */
26
+ export function canResolveTailwindStylesheet(entryCss, deps) {
27
+ if (deps.bun === undefined)
28
+ return false;
29
+ try {
30
+ deps.bun.resolveSync("tailwindcss", dirname(entryCss));
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
@@ -0,0 +1,43 @@
1
+ import { type EmailVerificationOptions, type PasswordResetOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
+ import { resolveSessionStore } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
3
+ import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
4
+ import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
5
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
6
+ import type { ConfigResolver, FeatureDefinition, Registry } from "@cosmicdrift/kumiko-framework/engine";
7
+ import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
8
+ import { type BootCrypto } from "./boot/boot-crypto.js";
9
+ import type { AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup } from "./run-prod-app.js";
10
+ export declare function addConfigAccessorFactory<T extends {
11
+ readonly configResolver?: ConfigResolver;
12
+ }>(resolved: T, registry: Registry): T;
13
+ export declare function buildBootExtraContext(opts: {
14
+ readonly db: DbConnection;
15
+ readonly features: readonly FeatureDefinition[];
16
+ readonly envSource: Record<string, string | undefined>;
17
+ readonly registry: Registry;
18
+ readonly hasAuth: boolean;
19
+ readonly crypto?: BootCrypto;
20
+ readonly masterKey?: MasterKeyProvider;
21
+ readonly sseBroker?: SseBroker;
22
+ readonly kms?: KmsAdapter;
23
+ }): Record<string, unknown>;
24
+ /** Die Auth-Felder die resolveAuthMail liest/normalisiert — beide
25
+ * App-Auth-Typen (prod + dev) erfüllen das strukturell. */
26
+ type AuthMailNormalizable = {
27
+ readonly mail?: AuthMailOptions;
28
+ readonly passwordReset?: PasswordResetSetup;
29
+ readonly emailVerification?: EmailVerificationSetup;
30
+ readonly signup?: SignupSetup;
31
+ readonly invite?: InviteSetup;
32
+ };
33
+ export declare function resolveAuthMail<T extends AuthMailNormalizable>(auth: T, hmacSecret: string, envSource: Record<string, string | undefined>): T & {
34
+ readonly passwordReset?: PasswordResetOptions;
35
+ readonly emailVerification?: EmailVerificationOptions;
36
+ };
37
+ export declare function buildProdSessionAuth(db: DbConnection, registry: Registry, sessionsFeature: FeatureDefinition | undefined, mfaFeature: FeatureDefinition | undefined, patFeature: FeatureDefinition | undefined): Promise<{
38
+ readonly sessionCreator: Awaited<ReturnType<typeof resolveSessionStore>>["creator"];
39
+ readonly sessionRevoker: Awaited<ReturnType<typeof resolveSessionStore>>["revoker"];
40
+ readonly sessionChecker: Awaited<ReturnType<typeof resolveSessionStore>>["checker"];
41
+ }>;
42
+ export declare function wireProdPatAutoRevoke(db: DbConnection, patFeature: FeatureDefinition): void;
43
+ export {};