@cosmicdrift/kumiko-server-runtime 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/boot-crypto.js +3 -3
- package/dist/boot/shared-boot-steps.d.ts +46 -0
- package/dist/boot/shared-boot-steps.js +160 -0
- package/dist/boot/wait-for-database.d.ts +5 -0
- package/dist/boot/wait-for-database.js +7 -6
- package/dist/boot/worker-boot-core.js +24 -38
- package/dist/boot-options/billing-option.d.ts +18 -9
- package/dist/boot-options/billing-option.js +44 -18
- package/dist/boot-options/demo-mode.d.ts +3 -3
- package/dist/boot-options/demo-mode.js +4 -1
- package/dist/boot-options/file-storage-option.js +6 -6
- package/dist/boot-options/option-env-schema.js +11 -2
- package/dist/boot-options/resolve-boot-options.d.ts +1 -0
- package/dist/boot-options/resolve-boot-options.js +11 -5
- package/dist/boot-options/stack-boot-requirements.d.ts +1 -1
- package/dist/boot-options/stack-boot-requirements.js +2 -14
- package/dist/build-prod-bundle.js +13 -13
- package/dist/bundled-lists-table.d.ts +2 -0
- package/dist/bundled-lists-table.js +56 -0
- package/dist/compose-auth-options.d.ts +16 -0
- package/dist/compose-auth-options.js +49 -0
- package/dist/compose-features.d.ts +5 -17
- package/dist/compose-features.js +6 -53
- package/dist/csp-hashes.js +1 -1
- package/dist/extra-routes-deps.d.ts +4 -0
- package/dist/extra-routes-deps.js +1 -0
- package/dist/fatal-error-handlers.js +7 -0
- package/dist/files-for-tenant.d.ts +3 -0
- package/dist/files-for-tenant.js +10 -0
- package/dist/renderer-web-fonts.js +3 -9
- package/dist/require-env.d.ts +2 -0
- package/dist/require-env.js +21 -0
- package/dist/resolve-installed-package.d.ts +10 -0
- package/dist/resolve-installed-package.js +32 -0
- package/dist/resolve-tailwind-cli.d.ts +3 -8
- package/dist/resolve-tailwind-cli.js +8 -25
- package/dist/run-bootstrap.d.ts +1 -1
- package/dist/run-bootstrap.js +1 -1
- package/dist/run-prod-app-auth.d.ts +17 -0
- package/dist/run-prod-app-auth.js +83 -0
- package/dist/run-prod-app-boot-context.d.ts +2 -0
- package/dist/run-prod-app-boot-context.js +15 -7
- package/dist/run-prod-app-config.d.ts +36 -0
- package/dist/run-prod-app-config.js +169 -0
- package/dist/run-prod-app-connections.d.ts +17 -0
- package/dist/run-prod-app-connections.js +59 -0
- package/dist/run-prod-app-entrypoint.d.ts +14 -0
- package/dist/run-prod-app-entrypoint.js +149 -0
- package/dist/run-prod-app-handle.d.ts +20 -0
- package/dist/run-prod-app-handle.js +105 -0
- package/dist/run-prod-app-request-context.d.ts +15 -0
- package/dist/run-prod-app-request-context.js +103 -0
- package/dist/run-prod-app-seeds.d.ts +13 -0
- package/dist/run-prod-app-seeds.js +88 -0
- package/dist/run-prod-app-static-files.js +9 -1
- package/dist/run-prod-app.d.ts +27 -19
- package/dist/run-prod-app.js +47 -727
- package/dist/run-worker-app.d.ts +12 -2
- package/dist/run-worker-app.js +2 -0
- package/package.json +27 -8
- package/src/changes.json +86 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BILLING_FOUNDATION_FEATURE } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
2
2
|
import { DELIVERY_FEATURE } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
3
3
|
import { FEATURE_TOGGLES_FEATURE } from "@cosmicdrift/kumiko-bundled-features/feature-toggles";
|
|
4
|
+
import { SUBSCRIPTION_DODO_FEATURE } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
|
|
4
5
|
import { SUBSCRIPTION_STRIPE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
5
6
|
import { TIER_ENGINE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
|
|
6
7
|
import { resolveBillingOption } from "./billing-option.js";
|
|
@@ -45,9 +46,9 @@ function wireFeatureToggles(options, mounted) {
|
|
|
45
46
|
function warnAboutBootOptions(options) {
|
|
46
47
|
const bootWarnings = stackBootWarnings(options).map((line) => `stacks in \`features\` expect boot options that are not set:\n${line}`);
|
|
47
48
|
if (options.runSingleInstance === false &&
|
|
48
|
-
(options.mail || options.fileStorage || options.featureToggles) &&
|
|
49
|
+
(options.mail || options.fileStorage || options.featureToggles || options.appOverrides) &&
|
|
49
50
|
options.workerMountsFeatures !== true) {
|
|
50
|
-
bootWarnings.push("runSingleInstance:false — delivery and file jobs run in runWorkerApp. Pass the same mail / fileStorage / featureToggles options there, or set workerMountsFeatures: true if it mounts those features itself.");
|
|
51
|
+
bootWarnings.push("runSingleInstance:false — delivery and file jobs run in runWorkerApp. Pass the same mail / fileStorage / featureToggles / appOverrides options there, or set workerMountsFeatures: true if it mounts those features itself.");
|
|
51
52
|
}
|
|
52
53
|
if (bootWarnings.length > 0) {
|
|
53
54
|
// biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
|
|
@@ -82,10 +83,15 @@ export async function resolveBootOptionWiring(options, envSource) {
|
|
|
82
83
|
envDefaults = { ...envDefaults, ...fileStorage.envDefaults };
|
|
83
84
|
}
|
|
84
85
|
if (options.billing) {
|
|
85
|
-
requireFeatures(mounted, "billing
|
|
86
|
-
|
|
86
|
+
requireFeatures(mounted, "billing", [BILLING_FOUNDATION_FEATURE, TIER_ENGINE_FEATURE], "Mount createBillingFoundationFeature() and createTierEngineFeature(), or composeSaasStack from @cosmicdrift/kumiko-dev-server/compose-stacks.");
|
|
87
|
+
if (options.billing.stripe) {
|
|
88
|
+
rejectDoubleMount(mounted, "billing.stripe", SUBSCRIPTION_STRIPE_FEATURE);
|
|
89
|
+
}
|
|
90
|
+
if (options.billing.dodo) {
|
|
91
|
+
rejectDoubleMount(mounted, "billing.dodo", SUBSCRIPTION_DODO_FEATURE);
|
|
92
|
+
}
|
|
87
93
|
const billing = resolveBillingOption(options.billing, envSource);
|
|
88
|
-
features.push(billing.
|
|
94
|
+
features.push(...billing.features);
|
|
89
95
|
extraRoutes.push(billing.route);
|
|
90
96
|
}
|
|
91
97
|
const featureToggles = wireFeatureToggles(options, mounted);
|
|
@@ -3,7 +3,7 @@ import type { RunProdAppOptions } from "../run-prod-app.ts";
|
|
|
3
3
|
export type StackBootOptionFields = Pick<RunProdAppOptions, "features" | "kms" | "allowPlaintextPii" | BootOptionName>;
|
|
4
4
|
/** Reads `bootRequires` from the raw feature list: dedupe may later drop the
|
|
5
5
|
* tagged copy of a feature two stacks share. `scope` limits the check to the
|
|
6
|
-
* options the calling boot function can set at all
|
|
6
|
+
* options the calling boot function can set at all. */
|
|
7
7
|
export declare function assertStackBootRequirements(options: StackBootOptionFields, scope?: readonly BootOptionName[]): void;
|
|
8
8
|
/** Requirements a stack marked `severity: "warn"`: logged at boot, never fatal. */
|
|
9
9
|
export declare function stackBootWarnings(options: StackBootOptionFields): string[];
|
|
@@ -23,24 +23,12 @@ const OPTION_CHECKS = {
|
|
|
23
23
|
},
|
|
24
24
|
billing: {
|
|
25
25
|
isSatisfied: (options, mounted) => options.billing !== undefined || mountsExtension(mounted, SUBSCRIPTION_PROVIDER_EXTENSION),
|
|
26
|
-
hint: "set billing: { stripe: { priceToTier }, defaultTier }",
|
|
26
|
+
hint: "set billing: { stripe: { priceToTier }, defaultTier } (or dodo)",
|
|
27
27
|
},
|
|
28
28
|
featureToggles: {
|
|
29
29
|
isSatisfied: (options) => options.featureToggles === true,
|
|
30
30
|
hint: "set featureToggles: true",
|
|
31
31
|
},
|
|
32
|
-
demoMode: {
|
|
33
|
-
isSatisfied: (options) => options.demoMode !== undefined,
|
|
34
|
-
hint: "set demoMode: { readOnly }",
|
|
35
|
-
},
|
|
36
|
-
seeds: {
|
|
37
|
-
isSatisfied: (options) => options.seeds !== undefined,
|
|
38
|
-
hint: "set seeds",
|
|
39
|
-
},
|
|
40
|
-
signals: {
|
|
41
|
-
isSatisfied: (options) => options.signals !== false,
|
|
42
|
-
hint: "drop signals: false",
|
|
43
|
-
},
|
|
44
32
|
rateLimit: {
|
|
45
33
|
isSatisfied: (options) => options.rateLimit !== false,
|
|
46
34
|
hint: "drop rateLimit: false",
|
|
@@ -65,7 +53,7 @@ function unsatisfiedRequirements(options, severity, scope) {
|
|
|
65
53
|
}
|
|
66
54
|
/** Reads `bootRequires` from the raw feature list: dedupe may later drop the
|
|
67
55
|
* tagged copy of a feature two stacks share. `scope` limits the check to the
|
|
68
|
-
* options the calling boot function can set at all
|
|
56
|
+
* options the calling boot function can set at all. */
|
|
69
57
|
export function assertStackBootRequirements(options, scope) {
|
|
70
58
|
const gaps = unsatisfiedRequirements(options, "error", scope);
|
|
71
59
|
if (gaps.length > 0) {
|
|
@@ -63,6 +63,7 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
|
63
63
|
import { BUNDLED_ASSETS_DIST_DIR, readBundledAssetDeclarations, resolveBundledAssetSource, } from "./bundled-assets.js";
|
|
64
64
|
import { CSP_HASHES_FILE, inlineScriptHashes } from "./csp-hashes.js";
|
|
65
65
|
import { RENDERER_WEB_FONT_FILE_PATTERN, RENDERER_WEB_FONTS_DIST_DIR, resolveRendererWebFontsDir, } from "./renderer-web-fonts.js";
|
|
66
|
+
import { resolveInstalledSync } from "./resolve-installed-package.js";
|
|
66
67
|
import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli.js";
|
|
67
68
|
// Bun runtime check as a module-level constant: every build step (Tailwind via
|
|
68
69
|
// Bun.spawn, client bundling via Bun.build, and stylesheet resolution via
|
|
@@ -454,18 +455,11 @@ function resolveStylesheetEntry(cwd, clientEntry, override) {
|
|
|
454
455
|
// and production builds behave identically.
|
|
455
456
|
if (!clientEntry)
|
|
456
457
|
return undefined;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", cwd);
|
|
461
|
-
if (!canResolveTailwindStylesheet(resolved, { bun: Bun, cwd })) {
|
|
462
|
-
return undefined;
|
|
463
|
-
}
|
|
464
|
-
return { path: resolved, isRendererWebFallback: true };
|
|
465
|
-
}
|
|
466
|
-
catch {
|
|
458
|
+
const resolved = resolveInstalledSync("@cosmicdrift/kumiko-renderer/web/styles.css", cwd);
|
|
459
|
+
if (resolved === undefined || !canResolveTailwindStylesheet(resolved, { bun: Bun, cwd })) {
|
|
467
460
|
return undefined;
|
|
468
461
|
}
|
|
462
|
+
return { path: resolved, isRendererWebFallback: true };
|
|
469
463
|
}
|
|
470
464
|
// Build-time guard against unstyled production output: when the build falls
|
|
471
465
|
// back to renderer-web/styles.css but the compiled CSS lacks the shell sentinel,
|
|
@@ -579,9 +573,15 @@ function isTailwindEngine(candidate) {
|
|
|
579
573
|
// Both packages are the CLI's own dependencies, so they resolve from the CLI package directory
|
|
580
574
|
// (same version the CLI would run), not from the app root.
|
|
581
575
|
async function loadTailwindEngine(cliPath, bun) {
|
|
582
|
-
const cliDir =
|
|
583
|
-
const
|
|
584
|
-
|
|
576
|
+
const cliDir = resolve(cliPath, "..", "..");
|
|
577
|
+
const importFromCli = (packageName) => {
|
|
578
|
+
const entry = resolveInstalledSync(packageName, cliDir, bun);
|
|
579
|
+
if (entry !== undefined)
|
|
580
|
+
return import(__rewriteRelativeImportExtension(entry));
|
|
581
|
+
throw new Error(`[kumiko build] ${packageName} is not installed next to @tailwindcss/cli (${cliDir}).`);
|
|
582
|
+
};
|
|
583
|
+
const node = await importFromCli("@tailwindcss/node");
|
|
584
|
+
const oxide = await importFromCli("@tailwindcss/oxide");
|
|
585
585
|
const engine = {
|
|
586
586
|
compile: isRecord(node) ? node["compile"] : undefined,
|
|
587
587
|
optimize: isRecord(node) ? node["optimize"] : undefined,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { normalizeListColumn, validateBundledListsOptions, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
export function applyBundledListsTable(features, options) {
|
|
3
|
+
if (options === undefined)
|
|
4
|
+
return [...features];
|
|
5
|
+
validateBundledListsOptions(options);
|
|
6
|
+
return features.map((feature) => feature.adoptsBundledLists === true ? withListTableDefaults(feature, options) : feature);
|
|
7
|
+
}
|
|
8
|
+
function withListTableDefaults(feature, options) {
|
|
9
|
+
return {
|
|
10
|
+
...feature,
|
|
11
|
+
screens: Object.fromEntries(Object.entries(feature.screens).map(([id, screen]) => [
|
|
12
|
+
id,
|
|
13
|
+
withScreenTableDefaults(screen, options),
|
|
14
|
+
])),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function withScreenTableDefaults(screen, options) {
|
|
18
|
+
switch (screen.type) {
|
|
19
|
+
case "entityList":
|
|
20
|
+
case "projectionList": {
|
|
21
|
+
const table = { ...options.table, ...screen.table };
|
|
22
|
+
return { ...screen, table, columns: pinFirstColumn(screen.columns, table, options) };
|
|
23
|
+
}
|
|
24
|
+
case "projectionDetail":
|
|
25
|
+
case "entityEdit":
|
|
26
|
+
return {
|
|
27
|
+
...screen,
|
|
28
|
+
layout: {
|
|
29
|
+
...screen.layout,
|
|
30
|
+
sections: screen.layout.sections.map((section) => withSectionTableDefaults(section, options)),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
default:
|
|
34
|
+
return screen;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function withSectionTableDefaults(section, options) {
|
|
38
|
+
if (section.kind !== "relatedList")
|
|
39
|
+
return section;
|
|
40
|
+
// export is only valid on entityList and projectionList
|
|
41
|
+
const { export: _export, ...defaultsWithoutExport } = options.table;
|
|
42
|
+
const table = { ...defaultsWithoutExport, ...section.table };
|
|
43
|
+
return { ...section, table, columns: pinFirstColumn(section.columns, table, options) };
|
|
44
|
+
}
|
|
45
|
+
function pinFirstColumn(columns, table, options) {
|
|
46
|
+
if (options.pinFirstColumn !== true || table.pinning !== true)
|
|
47
|
+
return columns;
|
|
48
|
+
const normalized = columns.map(normalizeListColumn);
|
|
49
|
+
if (normalized.some((column) => column.pinned !== undefined))
|
|
50
|
+
return columns;
|
|
51
|
+
// A hidden column only appears via the column picker; pinning it would surprise the user.
|
|
52
|
+
const firstVisible = normalized.findIndex((column) => column.hidden !== true);
|
|
53
|
+
if (firstVisible === -1)
|
|
54
|
+
return columns;
|
|
55
|
+
return columns.map((column, index) => index === firstVisible ? { ...normalizeListColumn(column), pinned: "left" } : column);
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AccountLockoutOptions, AccountUnlockOptions, AuthEmailPasswordOptions, EmailVerificationOptions, InviteOptions, PasswordResetOptions, SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
2
|
+
/** Shape of any run{Prod,Dev}App auth block that can carry a
|
|
3
|
+
* passwordReset/emailVerification config, once resolveAuthMail has resolved
|
|
4
|
+
* the wrapper's optional hmacSecret into the feature API's required one —
|
|
5
|
+
* a structural-typed lookup on the auth-only subset is enough. Lets
|
|
6
|
+
* buildComposeAuthOptions be called with both RunProd- and
|
|
7
|
+
* RunDev-AuthOptions without building the helper twice. */
|
|
8
|
+
export type AuthOptionsCarrier = {
|
|
9
|
+
readonly passwordReset?: PasswordResetOptions;
|
|
10
|
+
readonly emailVerification?: EmailVerificationOptions;
|
|
11
|
+
readonly signup?: SignupOptions;
|
|
12
|
+
readonly invite?: InviteOptions;
|
|
13
|
+
readonly accountUnlock?: AccountUnlockOptions;
|
|
14
|
+
readonly accountLockout?: AccountLockoutOptions;
|
|
15
|
+
};
|
|
16
|
+
export declare function buildComposeAuthOptions(auth: AuthOptionsCarrier | undefined): AuthEmailPasswordOptions | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The magic-link mail fields shared by all five flows. Conditional spreads omit
|
|
2
|
+
// undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
|
|
3
|
+
// trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
|
|
4
|
+
// reset/verify layer hmacSecret (+ mode) on top.
|
|
5
|
+
function pickMailFields(src) {
|
|
6
|
+
return {
|
|
7
|
+
appUrl: src.appUrl,
|
|
8
|
+
...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
|
|
9
|
+
...(src.appName !== undefined && { appName: src.appName }),
|
|
10
|
+
...(src.locale !== undefined && { locale: src.locale }),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function buildComposeAuthOptions(auth) {
|
|
14
|
+
if (!auth)
|
|
15
|
+
return undefined;
|
|
16
|
+
const opts = {};
|
|
17
|
+
if (auth.passwordReset) {
|
|
18
|
+
opts.passwordReset = {
|
|
19
|
+
hmacSecret: auth.passwordReset.hmacSecret,
|
|
20
|
+
...pickMailFields(auth.passwordReset),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (auth.emailVerification) {
|
|
24
|
+
opts.emailVerification = {
|
|
25
|
+
hmacSecret: auth.emailVerification.hmacSecret,
|
|
26
|
+
...pickMailFields(auth.emailVerification),
|
|
27
|
+
...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (auth.signup) {
|
|
31
|
+
opts.signup = pickMailFields(auth.signup);
|
|
32
|
+
}
|
|
33
|
+
if (auth.invite) {
|
|
34
|
+
opts.invite = pickMailFields(auth.invite);
|
|
35
|
+
}
|
|
36
|
+
if (auth.accountUnlock) {
|
|
37
|
+
opts.accountUnlock = {
|
|
38
|
+
hmacSecret: auth.accountUnlock.hmacSecret,
|
|
39
|
+
...pickMailFields(auth.accountUnlock),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (auth.accountLockout) {
|
|
43
|
+
opts.accountLockout = auth.accountLockout;
|
|
44
|
+
}
|
|
45
|
+
return hasAnyAuthFlow(opts) ? opts : undefined;
|
|
46
|
+
}
|
|
47
|
+
function hasAnyAuthFlow(opts) {
|
|
48
|
+
return Object.values(opts).some(Boolean);
|
|
49
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { type AuthEmailPasswordOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
2
|
+
import { type BundledListsOptions, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
export { type AuthOptionsCarrier, buildComposeAuthOptions } from "./compose-auth-options.ts";
|
|
3
4
|
export type ComposeFeaturesOptions = {
|
|
4
5
|
/** When true, prepends config + user + tenant + auth-email-password
|
|
5
6
|
* (+ auth-self-registration when authOptions.signup is set) before the
|
|
@@ -12,20 +13,7 @@ export type ComposeFeaturesOptions = {
|
|
|
12
13
|
* auth-routes.ts but no handler dispatches). Goes hand-in-hand with the
|
|
13
14
|
* passwordReset block in RunProdAppAuthOptions / RunDevAppAuthOptions. */
|
|
14
15
|
readonly authOptions?: AuthEmailPasswordOptions;
|
|
16
|
+
/** One table block for every list screen and relatedList of the bundled features; screen-own keys win. */
|
|
17
|
+
readonly bundledLists?: BundledListsOptions;
|
|
15
18
|
};
|
|
16
19
|
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;
|
package/dist/compose-features.js
CHANGED
|
@@ -21,9 +21,11 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
|
|
|
21
21
|
import { collectAssignableAppRoles, createTenantFeature, } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
22
22
|
import { TENANT_LIFECYCLE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
|
|
23
23
|
import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
24
|
-
import { dedupeFeatures } from "@cosmicdrift/kumiko-framework/engine";
|
|
24
|
+
import { dedupeFeatures, } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
25
|
import { FILES_FEATURE } from "@cosmicdrift/kumiko-framework/files";
|
|
26
26
|
import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
|
|
27
|
+
import { applyBundledListsTable } from "./bundled-lists-table.js";
|
|
28
|
+
export { buildComposeAuthOptions } from "./compose-auth-options.js";
|
|
27
29
|
export function composeFeatures(appFeatures, options) {
|
|
28
30
|
// ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
|
|
29
31
|
// an app composing its own foundation and mounting auth-mfa itself is
|
|
@@ -32,7 +34,7 @@ export function composeFeatures(appFeatures, options) {
|
|
|
32
34
|
// bypasses MFA. Upgrade if this trips someone: warn here when appFeatures
|
|
33
35
|
// contains AUTH_MFA_FEATURE but includeBundled is false.
|
|
34
36
|
if (!options.includeBundled)
|
|
35
|
-
return dedupeFeatures(appFeatures);
|
|
37
|
+
return applyBundledListsTable(dedupeFeatures(appFeatures), options.bundledLists);
|
|
36
38
|
// Bundled foundation goes first so its instances carry the runDevApp /
|
|
37
39
|
// runProdApp `authOptions` (passwordReset wiring etc.). App-features that
|
|
38
40
|
// ALSO declare one of these names — e.g. the create-kumiko-app picker
|
|
@@ -92,12 +94,12 @@ export function composeFeatures(appFeatures, options) {
|
|
|
92
94
|
const needsFilesTenantData = names.has(FILES_FEATURE) &&
|
|
93
95
|
names.has(TENANT_LIFECYCLE_FEATURE) &&
|
|
94
96
|
!names.has(FILES_TENANT_DATA_FEATURE);
|
|
95
|
-
return [
|
|
97
|
+
return applyBundledListsTable([
|
|
96
98
|
...bundled,
|
|
97
99
|
...missingAuthMailStack(names),
|
|
98
100
|
...filteredApp,
|
|
99
101
|
...(needsFilesTenantData ? [createFilesTenantDataFeature()] : []),
|
|
100
|
-
];
|
|
102
|
+
], options.bundledLists);
|
|
101
103
|
}
|
|
102
104
|
// auth-email-password declares its mails as stored templates, and the boot seed
|
|
103
105
|
// fails without the template-resolver and a mail renderer. Mounting the missing
|
|
@@ -110,52 +112,3 @@ function missingAuthMailStack(mountedNames) {
|
|
|
110
112
|
createRendererMailHtmlFeature(),
|
|
111
113
|
].filter((feature) => !mountedNames.has(feature.name));
|
|
112
114
|
}
|
|
113
|
-
// The magic-link mail fields shared by all five flows. Conditional spreads omit
|
|
114
|
-
// undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
|
|
115
|
-
// trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
|
|
116
|
-
// reset/verify layer hmacSecret (+ mode) on top.
|
|
117
|
-
function pickMailFields(src) {
|
|
118
|
-
return {
|
|
119
|
-
appUrl: src.appUrl,
|
|
120
|
-
...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
|
|
121
|
-
...(src.appName !== undefined && { appName: src.appName }),
|
|
122
|
-
...(src.locale !== undefined && { locale: src.locale }),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
export function buildComposeAuthOptions(auth) {
|
|
126
|
-
if (!auth)
|
|
127
|
-
return undefined;
|
|
128
|
-
const opts = {};
|
|
129
|
-
if (auth.passwordReset) {
|
|
130
|
-
opts.passwordReset = {
|
|
131
|
-
hmacSecret: auth.passwordReset.hmacSecret,
|
|
132
|
-
...pickMailFields(auth.passwordReset),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
if (auth.emailVerification) {
|
|
136
|
-
opts.emailVerification = {
|
|
137
|
-
hmacSecret: auth.emailVerification.hmacSecret,
|
|
138
|
-
...pickMailFields(auth.emailVerification),
|
|
139
|
-
...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (auth.signup) {
|
|
143
|
-
opts.signup = pickMailFields(auth.signup);
|
|
144
|
-
}
|
|
145
|
-
if (auth.invite) {
|
|
146
|
-
opts.invite = pickMailFields(auth.invite);
|
|
147
|
-
}
|
|
148
|
-
if (auth.accountUnlock) {
|
|
149
|
-
opts.accountUnlock = {
|
|
150
|
-
hmacSecret: auth.accountUnlock.hmacSecret,
|
|
151
|
-
...pickMailFields(auth.accountUnlock),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (auth.accountLockout) {
|
|
155
|
-
opts.accountLockout = auth.accountLockout;
|
|
156
|
-
}
|
|
157
|
-
return hasAnyAuthFlow(opts) ? opts : undefined;
|
|
158
|
-
}
|
|
159
|
-
function hasAnyAuthFlow(opts) {
|
|
160
|
-
return Object.values(opts).some(Boolean);
|
|
161
|
-
}
|
package/dist/csp-hashes.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { isPlainObject, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
3
3
|
import { isScriptHash } from "./security-headers.js";
|
|
4
4
|
export const CSP_HASHES_FILE = "csp-hashes.json";
|
|
5
|
-
const INLINE_SCRIPT_PATTERN = /<script\b([
|
|
5
|
+
const INLINE_SCRIPT_PATTERN = /<script\b((?:"[^"]*"|'[^']*'|[^'">])*)>([\s\S]*?)<\/script\s*>/gi;
|
|
6
6
|
const SRC_ATTRIBUTE_PATTERN = /\ssrc\s*=/i;
|
|
7
7
|
const TYPE_ATTRIBUTE_PATTERN = /\stype\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
8
8
|
// Data blocks (application/json, ld+json, templates) never execute, so
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { Registry, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
5
|
import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
|
|
5
6
|
import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
6
7
|
import type { Redis } from "ioredis";
|
|
7
8
|
export type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
|
|
8
9
|
export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
|
|
10
|
+
export { makeFilesForTenant } from "./files-for-tenant.ts";
|
|
9
11
|
/** Deps for the `wire` hook (runProdApp/createKumikoServer, after
|
|
10
12
|
* buildServer has mounted `extraRoutes`) and
|
|
11
13
|
* for `runWorkerApp.wireComponents`. Naming: `deps` not `ctx` because
|
|
@@ -18,6 +20,8 @@ export type SystemWireDeps = {
|
|
|
18
20
|
/** Feature-registry — e.g. for plugin-lookups via
|
|
19
21
|
* `registry.getExtensionUsages("subscriptionProvider")`. */
|
|
20
22
|
readonly registry: Registry;
|
|
23
|
+
/** File context of a tenant's storage; undefined when no file storage is mounted. Required so a consumer cannot lose attachment reading by omission. */
|
|
24
|
+
readonly filesForTenant: (tenantId: string) => FileContext | undefined;
|
|
21
25
|
/** The boot's master key keyring; absent when no `KUMIKO_SECRETS_MASTER_KEY_V<n>` is set. */
|
|
22
26
|
readonly masterKeyProvider?: MasterKeyProvider;
|
|
23
27
|
/** Cipher for `encrypted: true` config keys, same instance and DEK cache as the handlers use. */
|
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
// truth lives in framework/api next to buildServer's own `extraRoutes`
|
|
3
3
|
// mount so the two never drift on how the SystemAdmin identity is built.
|
|
4
4
|
export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
|
|
5
|
+
export { makeFilesForTenant } from "./files-for-tenant.js";
|
|
@@ -16,6 +16,7 @@ export function installFatalErrorHandlers(options) {
|
|
|
16
16
|
const exit = options.exit ?? ((code) => process.exit(code));
|
|
17
17
|
const hardExitMs = options.hardExitMs ?? DEFAULT_HARD_EXIT_MS;
|
|
18
18
|
let handling = false;
|
|
19
|
+
let stopRunning = false;
|
|
19
20
|
const handle = (source) => (reason) => {
|
|
20
21
|
// skip: a second fault during shutdown is already covered by the running stop and hard-exit timer
|
|
21
22
|
if (handling)
|
|
@@ -28,11 +29,13 @@ export function installFatalErrorHandlers(options) {
|
|
|
28
29
|
});
|
|
29
30
|
const hardExitTimer = setTimeout(() => exit(1), hardExitMs);
|
|
30
31
|
hardExitTimer.unref();
|
|
32
|
+
stopRunning = true;
|
|
31
33
|
void stop()
|
|
32
34
|
.catch((stopError) => {
|
|
33
35
|
logger.error("shutdown after fatal error failed", describeFatal(stopError));
|
|
34
36
|
})
|
|
35
37
|
.finally(() => {
|
|
38
|
+
stopRunning = false;
|
|
36
39
|
clearTimeout(hardExitTimer);
|
|
37
40
|
exit(1);
|
|
38
41
|
});
|
|
@@ -42,6 +45,10 @@ export function installFatalErrorHandlers(options) {
|
|
|
42
45
|
process.on("uncaughtException", onUncaughtException);
|
|
43
46
|
process.on("unhandledRejection", onUnhandledRejection);
|
|
44
47
|
return () => {
|
|
48
|
+
// stop() uninstalls first; during a fatal shutdown the listeners must stay so a second fault cannot hit the default crash path mid-flush
|
|
49
|
+
// skip: fatal shutdown in progress, see above.
|
|
50
|
+
if (stopRunning)
|
|
51
|
+
return;
|
|
45
52
|
process.off("uncaughtException", onUncaughtException);
|
|
46
53
|
process.off("unhandledRejection", onUnhandledRejection);
|
|
47
54
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileContextFromResolver } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
|
|
2
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
// Takes the AppContext buildServer returned: only that one carries `_fileProviderResolver`.
|
|
4
|
+
// Without a resolver, `context.files` is bound to the system tenant and reaches no other one.
|
|
5
|
+
export function makeFilesForTenant(context) {
|
|
6
|
+
const resolver = context._fileProviderResolver;
|
|
7
|
+
if (resolver)
|
|
8
|
+
return (tenantId) => fileContextFromResolver(resolver, tenantId);
|
|
9
|
+
return (tenantId) => (tenantId === SYSTEM_TENANT_ID ? context.files : undefined);
|
|
10
|
+
}
|
|
@@ -3,26 +3,20 @@
|
|
|
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
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { resolveInstalledSync } from "./resolve-installed-package.js";
|
|
6
7
|
export const RENDERER_WEB_FONTS_URL_PREFIX = "/assets/kumiko/fonts/";
|
|
7
8
|
/** dist-relative folder the prod build copies the fonts into. */
|
|
8
9
|
export const RENDERER_WEB_FONTS_DIST_DIR = "assets/kumiko/fonts";
|
|
9
10
|
/** Allowlist for servable font file names; also rules out path segments. */
|
|
10
11
|
export const RENDERER_WEB_FONT_FILE_PATTERN = /^[a-z0-9-]+\.woff2$/;
|
|
11
|
-
const hasBun = typeof Bun !== "undefined";
|
|
12
12
|
/** Directory of the woff2 files next to renderer-web's styles.css, or
|
|
13
13
|
* undefined when the package can't be resolved from any `searchFrom` dir
|
|
14
14
|
* (or outside Bun, which does the package-exports resolution). */
|
|
15
15
|
export function resolveRendererWebFontsDir(searchFrom) {
|
|
16
|
-
if (!hasBun)
|
|
17
|
-
return undefined;
|
|
18
16
|
for (const from of searchFrom) {
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const stylesheet = resolveInstalledSync("@cosmicdrift/kumiko-renderer/web/styles.css", from);
|
|
18
|
+
if (stylesheet !== undefined)
|
|
21
19
|
return join(dirname(stylesheet), "fonts");
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
// try the next search root
|
|
25
|
-
}
|
|
26
20
|
}
|
|
27
21
|
return undefined;
|
|
28
22
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Strict env-var read. Throws with a clear hint when missing — better
|
|
2
|
+
// than discovering a Postgres-connection-refused 30s into the boot.
|
|
3
|
+
// `src` defaults to process.env but is threaded from the caller's envSource
|
|
4
|
+
// so the boot-path reads the SAME env-quelle that was validated above —
|
|
5
|
+
// injected dummies in test-mode must not silently fall back to process.env.
|
|
6
|
+
export function requireEnv(name, src = process.env, context = "runProdApp") {
|
|
7
|
+
const value = src[name];
|
|
8
|
+
if (value === undefined || value === "") {
|
|
9
|
+
const advice = context === "runProdApp"
|
|
10
|
+
? "Set it in your container env / .env.production / Coolify secrets."
|
|
11
|
+
: "Set it in your .env / shell before running the dev server.";
|
|
12
|
+
throw new Error(`${context}: required env var "${name}" is missing or empty. ${advice}`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
// Optional env helper — returns undefined for missing, string for set.
|
|
17
|
+
// Used for KUMIKO_INSTANCE_ID, JWT_ISSUER and other "nice to have" knobs.
|
|
18
|
+
export function readEnv(name, src = process.env) {
|
|
19
|
+
const value = src[name];
|
|
20
|
+
return value === undefined || value === "" ? undefined : value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type BunResolver = {
|
|
2
|
+
readonly resolveSync: (id: string, from: string) => string;
|
|
3
|
+
};
|
|
4
|
+
export declare function packageNameOf(specifier: string): string;
|
|
5
|
+
/** Walks node_modules upwards like Node does; returns the real path so transitive deps
|
|
6
|
+
* of an isolated-linker install are found as siblings. */
|
|
7
|
+
export declare function findInstalledPackageDir(packageName: string, fromDir: string): string | undefined;
|
|
8
|
+
/** Bun.resolveSync auto-installs from the registry (since 1.4.4) when no node_modules tree
|
|
9
|
+
* exists, so only call it for packages that are already installed. */
|
|
10
|
+
export declare function resolveInstalledSync(specifier: string, fromDir: string, bun?: BunResolver | undefined): string | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
export function packageNameOf(specifier) {
|
|
4
|
+
const segments = specifier.split("/");
|
|
5
|
+
return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : (segments[0] ?? specifier);
|
|
6
|
+
}
|
|
7
|
+
/** Walks node_modules upwards like Node does; returns the real path so transitive deps
|
|
8
|
+
* of an isolated-linker install are found as siblings. */
|
|
9
|
+
export function findInstalledPackageDir(packageName, fromDir) {
|
|
10
|
+
for (let dir = resolve(fromDir);; dir = resolve(dir, "..")) {
|
|
11
|
+
const packageDir = join(dir, "node_modules", packageName);
|
|
12
|
+
if (existsSync(join(packageDir, "package.json")))
|
|
13
|
+
return realpathSync(packageDir);
|
|
14
|
+
if (resolve(dir, "..") === dir)
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const globalBun = typeof Bun !== "undefined" ? Bun : undefined;
|
|
19
|
+
/** Bun.resolveSync auto-installs from the registry (since 1.4.4) when no node_modules tree
|
|
20
|
+
* exists, so only call it for packages that are already installed. */
|
|
21
|
+
export function resolveInstalledSync(specifier, fromDir, bun = globalBun) {
|
|
22
|
+
if (bun === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (findInstalledPackageDir(packageNameOf(specifier), fromDir) === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
try {
|
|
27
|
+
return bun.resolveSync(specifier, fromDir);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
type BunResolver
|
|
2
|
-
resolveSync: (id: string, from: string) => string;
|
|
3
|
-
};
|
|
1
|
+
import { type BunResolver } from "./resolve-installed-package.ts";
|
|
4
2
|
export type ResolveTailwindCliDeps = {
|
|
5
3
|
readonly bun?: BunResolver;
|
|
6
4
|
readonly cwd: string;
|
|
7
5
|
};
|
|
8
6
|
export declare function resolveTailwindCli(deps: ResolveTailwindCliDeps): string | undefined;
|
|
9
|
-
/** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
|
|
10
|
-
*
|
|
11
|
-
* aren't resolvable from that directory — e.g. Bun cache copies of
|
|
12
|
-
* `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
|
|
7
|
+
/** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry CSS file, so
|
|
8
|
+
* tailwindcss must be installed above it (not just next to a Bun cache copy of renderer-web). */
|
|
13
9
|
export declare function canResolveTailwindStylesheet(entryCss: string, deps: ResolveTailwindCliDeps): boolean;
|
|
14
|
-
export {};
|