@cosmicdrift/kumiko-server-runtime 0.388.0 → 0.390.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/apply-boot-seeds.js +11 -8
- package/dist/boot/job-run-logger.js +1 -1
- package/dist/boot/shared-boot-steps.d.ts +8 -5
- package/dist/boot/shared-boot-steps.js +26 -16
- package/dist/boot/stream-tenant-backfill-on-boot.d.ts +27 -0
- package/dist/boot/stream-tenant-backfill-on-boot.js +161 -0
- package/dist/boot/wait-for-database.d.ts +2 -0
- package/dist/boot/wait-for-database.js +4 -3
- package/dist/boot/warn-row-level-security-off.d.ts +3 -0
- package/dist/boot/warn-row-level-security-off.js +6 -0
- package/dist/boot/worker-boot-core.d.ts +2 -1
- package/dist/boot/worker-boot-core.js +28 -5
- package/dist/boot/worker-metrics-server.js +2 -0
- package/dist/boot-options/demo-mode.d.ts +3 -3
- package/dist/boot-options/demo-mode.js +4 -1
- package/dist/boot-options/resolve-boot-options.d.ts +1 -0
- package/dist/boot-options/resolve-boot-options.js +2 -2
- package/dist/boot-options/seed-shortcuts.js +3 -1
- package/dist/boot-options/stack-boot-requirements.d.ts +1 -1
- package/dist/boot-options/stack-boot-requirements.js +1 -13
- 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 +13 -53
- package/dist/extra-routes-deps.d.ts +5 -1
- package/dist/extra-routes-deps.js +1 -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/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-prod-app-boot-context.d.ts +2 -0
- package/dist/run-prod-app-boot-context.js +11 -9
- package/dist/run-prod-app-config.d.ts +2 -0
- package/dist/run-prod-app-config.js +3 -1
- package/dist/run-prod-app-connections.d.ts +2 -1
- package/dist/run-prod-app-connections.js +11 -7
- package/dist/run-prod-app-entrypoint.d.ts +1 -1
- package/dist/run-prod-app-entrypoint.js +6 -2
- package/dist/run-prod-app-handle.js +4 -1
- package/dist/run-prod-app-request-context.js +9 -2
- package/dist/run-prod-app-seeds.js +39 -24
- package/dist/run-prod-app-static-files.js +9 -1
- package/dist/run-prod-app.d.ts +19 -4
- package/dist/run-prod-app.js +7 -3
- package/dist/run-worker-app.d.ts +22 -4
- package/dist/run-worker-app.js +22 -4
- package/dist/seed-dispatcher.js +3 -1
- package/package.json +19 -8
- package/src/changes.json +92 -0
|
@@ -29,18 +29,6 @@ const OPTION_CHECKS = {
|
|
|
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,14 @@ 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";
|
|
29
|
+
// Output of a first composeFeatures pass comes back as app features when a caller composes twice (the app
|
|
30
|
+
// config already composed, boot composes again); those copies are dropped silently, only app-supplied ones warn.
|
|
31
|
+
const composedBundledInstances = new WeakSet();
|
|
27
32
|
export function composeFeatures(appFeatures, options) {
|
|
28
33
|
// ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
|
|
29
34
|
// an app composing its own foundation and mounting auth-mfa itself is
|
|
@@ -32,7 +37,7 @@ export function composeFeatures(appFeatures, options) {
|
|
|
32
37
|
// bypasses MFA. Upgrade if this trips someone: warn here when appFeatures
|
|
33
38
|
// contains AUTH_MFA_FEATURE but includeBundled is false.
|
|
34
39
|
if (!options.includeBundled)
|
|
35
|
-
return dedupeFeatures(appFeatures);
|
|
40
|
+
return applyBundledListsTable(dedupeFeatures(appFeatures), options.bundledLists);
|
|
36
41
|
// Bundled foundation goes first so its instances carry the runDevApp /
|
|
37
42
|
// runProdApp `authOptions` (passwordReset wiring etc.). App-features that
|
|
38
43
|
// ALSO declare one of these names — e.g. the create-kumiko-app picker
|
|
@@ -72,6 +77,8 @@ export function composeFeatures(appFeatures, options) {
|
|
|
72
77
|
// "on unless an operator flips it off at runtime" contract.
|
|
73
78
|
...(authOptions?.signup !== undefined ? [createAuthSelfRegistrationToggleFeature()] : []),
|
|
74
79
|
];
|
|
80
|
+
for (const f of bundled)
|
|
81
|
+
composedBundledInstances.add(f);
|
|
75
82
|
const bundledNames = new Set(bundled.map((f) => f.name));
|
|
76
83
|
const filteredApp = [];
|
|
77
84
|
for (const f of dedupedAppFeatures) {
|
|
@@ -81,6 +88,8 @@ export function composeFeatures(appFeatures, options) {
|
|
|
81
88
|
// authOptions.signup and only get it from there.
|
|
82
89
|
if (f.name === AUTH_SELF_REGISTRATION_FEATURE)
|
|
83
90
|
continue;
|
|
91
|
+
if (composedBundledInstances.has(f))
|
|
92
|
+
continue;
|
|
84
93
|
// biome-ignore lint/suspicious/noConsole: boot-time UX warning
|
|
85
94
|
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.`);
|
|
86
95
|
continue;
|
|
@@ -92,12 +101,12 @@ export function composeFeatures(appFeatures, options) {
|
|
|
92
101
|
const needsFilesTenantData = names.has(FILES_FEATURE) &&
|
|
93
102
|
names.has(TENANT_LIFECYCLE_FEATURE) &&
|
|
94
103
|
!names.has(FILES_TENANT_DATA_FEATURE);
|
|
95
|
-
return [
|
|
104
|
+
return applyBundledListsTable([
|
|
96
105
|
...bundled,
|
|
97
106
|
...missingAuthMailStack(names),
|
|
98
107
|
...filteredApp,
|
|
99
108
|
...(needsFilesTenantData ? [createFilesTenantDataFeature()] : []),
|
|
100
|
-
];
|
|
109
|
+
], options.bundledLists);
|
|
101
110
|
}
|
|
102
111
|
// auth-email-password declares its mails as stored templates, and the boot seed
|
|
103
112
|
// fails without the template-resolver and a mail renderer. Mounting the missing
|
|
@@ -110,52 +119,3 @@ function missingAuthMailStack(mountedNames) {
|
|
|
110
119
|
createRendererMailHtmlFeature(),
|
|
111
120
|
].filter((feature) => !mountedNames.has(feature.name));
|
|
112
121
|
}
|
|
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
|
-
}
|
|
@@ -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,11 +20,13 @@ 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. */
|
|
24
28
|
readonly configEncryption?: EnvelopeCipher;
|
|
25
|
-
/** Same contract as `ctx.metricsFor`: unregistered metric names no-op. */
|
|
29
|
+
/** Same contract as `ctx.obs.metricsFor`: unregistered metric names no-op. */
|
|
26
30
|
readonly metricsFor: (featureName: string) => MetricsHandle;
|
|
27
31
|
/** Writes through the /api/*-command-dispatcher (same idempotency/job
|
|
28
32
|
* hooks) as an auto-constructed SystemAdmin of the target tenant,
|
|
@@ -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";
|
|
@@ -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,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 {};
|
|
@@ -1,34 +1,17 @@
|
|
|
1
1
|
// Resolves the locally installed @tailwindcss/cli binary to its absolute path.
|
|
2
2
|
// We avoid `bunx` because it still requests the registry manifest for a locally
|
|
3
3
|
// installed package and fails with `FailedToOpenSocket` when offline.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// branches can be tested under Vitest/Node without booting the server.
|
|
7
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { findInstalledPackageDir } from "./resolve-installed-package.js";
|
|
8
6
|
export function resolveTailwindCli(deps) {
|
|
9
7
|
if (deps.bun === undefined)
|
|
10
8
|
return undefined;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// bin: { tailwindcss: "./dist/index.mjs" } → absoluter Pfad
|
|
14
|
-
return resolve(pkgJsonPath, "..", "dist", "index.mjs");
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
9
|
+
const cliDir = findInstalledPackageDir("@tailwindcss/cli", deps.cwd);
|
|
10
|
+
return cliDir === undefined ? undefined : join(cliDir, "dist", "index.mjs");
|
|
19
11
|
}
|
|
20
|
-
/** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
|
|
21
|
-
*
|
|
22
|
-
* aren't resolvable from that directory — e.g. Bun cache copies of
|
|
23
|
-
* `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
|
|
12
|
+
/** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry CSS file, so
|
|
13
|
+
* tailwindcss must be installed above it (not just next to a Bun cache copy of renderer-web). */
|
|
24
14
|
export function canResolveTailwindStylesheet(entryCss, deps) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
deps.bun.resolveSync("tailwindcss", dirname(entryCss));
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
15
|
+
return (deps.bun !== undefined &&
|
|
16
|
+
findInstalledPackageDir("tailwindcss", dirname(entryCss)) !== undefined);
|
|
34
17
|
}
|
|
@@ -16,6 +16,8 @@ export declare function addConfigAccessorFactory<T extends {
|
|
|
16
16
|
export declare function loggerFromExtraContext(extraContext: Record<string, unknown>): Logger | undefined;
|
|
17
17
|
/** Config key (`feature:config:name`) to value; wins over the key's env variable. */
|
|
18
18
|
export type ConfigOverridesInput = Readonly<Record<string, string | number | boolean>>;
|
|
19
|
+
/** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
|
|
20
|
+
export declare function buildConfigOverrides(registry: Registry, envSource: Record<string, string | undefined>, appOverrides: ConfigOverridesInput | undefined): import("@cosmicdrift/kumiko-bundled-features/config").AppConfigOverrides;
|
|
19
21
|
type BootExtraContextOptions = {
|
|
20
22
|
readonly db: DbConnection;
|
|
21
23
|
readonly features: readonly FeatureDefinition[];
|
|
@@ -23,13 +23,11 @@ import { resolveBootCrypto } from "./boot/boot-crypto.js";
|
|
|
23
23
|
export function addConfigAccessorFactory(resolved, registry) {
|
|
24
24
|
if (!resolved.configResolver)
|
|
25
25
|
return resolved;
|
|
26
|
-
//
|
|
27
|
-
// effective resolver; the underscore alias stays until the next major.
|
|
26
|
+
// Always overrides an app-supplied value: the factory must derive from the effective resolver.
|
|
28
27
|
const configAccessorFactory = createConfigAccessorFactory(registry, resolved.configResolver);
|
|
29
28
|
return {
|
|
30
29
|
...resolved,
|
|
31
30
|
configAccessorFactory,
|
|
32
|
-
_configAccessorFactory: configAccessorFactory,
|
|
33
31
|
};
|
|
34
32
|
}
|
|
35
33
|
// Framework-default providers for AppContext, using the same mechanism as
|
|
@@ -42,7 +40,7 @@ export function addConfigAccessorFactory(resolved, registry) {
|
|
|
42
40
|
// boot/boot-crypto.ts (envHasMasterKek, resolveBootCrypto), shared with runDevApp.
|
|
43
41
|
// A production misconfiguration (secrets mounted without a KEK) is caught by
|
|
44
42
|
// secretsEnvSchema during boot; without the env-schema path, requireSecretsContext
|
|
45
|
-
// throws with a wiring hint on the first ctx.secrets access. configResolver is only
|
|
43
|
+
// throws with a wiring hint on the first ctx.services.secrets access. configResolver is only
|
|
46
44
|
// wired in auth mode. Exported and pure for unit tests; the caller merges it with
|
|
47
45
|
// app values (the app wins).
|
|
48
46
|
// Prod/dev parity for ctx.notify: without this `_notifyFactory` is only wired
|
|
@@ -116,16 +114,20 @@ function bootCryptoContext(crypto) {
|
|
|
116
114
|
...(crypto.masterKeyProvider && { masterKeyProvider: crypto.masterKeyProvider }),
|
|
117
115
|
// Encrypt/decrypt partner for `encrypted: true` config keys. Wired
|
|
118
116
|
// whenever a master key exists — NOT gated on the secrets feature,
|
|
119
|
-
// config encryption must work without mounting ctx.secrets.
|
|
117
|
+
// config encryption must work without mounting ctx.services.secrets.
|
|
120
118
|
...(crypto.configCipher && { configEncryption: crypto.configCipher }),
|
|
121
119
|
};
|
|
122
120
|
}
|
|
123
|
-
|
|
121
|
+
/** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
|
|
122
|
+
export function buildConfigOverrides(registry, envSource, appOverrides) {
|
|
124
123
|
const envOverrides = buildEnvConfigOverrides(registry, envSource);
|
|
124
|
+
return appOverrides
|
|
125
|
+
? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
|
|
126
|
+
: envOverrides;
|
|
127
|
+
}
|
|
128
|
+
function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
|
|
125
129
|
return createConfigResolver({
|
|
126
|
-
appOverrides: appOverrides
|
|
127
|
-
? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
|
|
128
|
-
: envOverrides,
|
|
130
|
+
appOverrides: buildConfigOverrides(registry, envSource, appOverrides),
|
|
129
131
|
...(crypto.configCipher && { cipher: crypto.configCipher }),
|
|
130
132
|
});
|
|
131
133
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadJwtSecretOrKeyring } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import type { RowLevelSecurityMode } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { type FeatureDefinition, type Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
4
5
|
import { resolveBootCrypto } from "./boot/boot-crypto.ts";
|
|
@@ -15,6 +16,7 @@ export type ProdBootConfig = {
|
|
|
15
16
|
readonly envSource: Record<string, string | undefined>;
|
|
16
17
|
readonly bootKms: BootOptionWiring["kms"];
|
|
17
18
|
readonly databaseUrl: string;
|
|
19
|
+
readonly rowLevelSecurity: RowLevelSecurityMode;
|
|
18
20
|
readonly redisUrl: string;
|
|
19
21
|
readonly jwtSecret: string;
|
|
20
22
|
readonly jwtSecretOrKeyring: ReturnType<typeof loadJwtSecretOrKeyring>;
|
|
@@ -2,6 +2,7 @@ import { EXT_SESSION_STORE } from "@cosmicdrift/kumiko-bundled-features/auth-fou
|
|
|
2
2
|
import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
3
3
|
import { loadJwtSecretOrKeyring, TRUSTED_PROXY_HOPS_ENV } from "@cosmicdrift/kumiko-framework/api";
|
|
4
4
|
import { createRegistry, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import { DEFAULT_ROW_LEVEL_SECURITY_MODE } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
5
6
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
6
7
|
import { resolveBootCrypto } from "./boot/boot-crypto.js";
|
|
7
8
|
import { composeAppFeatures, validateComposedFeatures, withEnvDefaults, } from "./boot/shared-boot-steps.js";
|
|
@@ -100,6 +101,7 @@ export async function resolveProdBootConfig(options, decryptedEnvSource, log, re
|
|
|
100
101
|
envSource,
|
|
101
102
|
bootKms,
|
|
102
103
|
databaseUrl,
|
|
104
|
+
rowLevelSecurity: options.rowLevelSecurity ?? DEFAULT_ROW_LEVEL_SECURITY_MODE,
|
|
103
105
|
redisUrl,
|
|
104
106
|
jwtSecret,
|
|
105
107
|
jwtSecretOrKeyring,
|
|
@@ -136,7 +138,7 @@ export function composeProdFeatures(options, config, log) {
|
|
|
136
138
|
// auth-email-password via composeFeatures, using the same feature list as
|
|
137
139
|
// runDevApp and the per-app schema generator. Build it before any connection
|
|
138
140
|
// so boot mode can validate wiring without opening Postgres or Redis sockets.
|
|
139
|
-
const features = composeAppFeatures(options.features, bootOptionWiring.features, effectiveAuth);
|
|
141
|
+
const features = composeAppFeatures(options.features, bootOptionWiring.features, effectiveAuth, options.bundledLists);
|
|
140
142
|
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
141
143
|
validateComposedFeatures(features, {
|
|
142
144
|
env: envSource,
|