@cosmicdrift/kumiko-server-runtime 0.364.0 → 0.365.1
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/wait-for-database.d.ts +13 -0
- package/dist/boot/wait-for-database.js +64 -0
- package/dist/boot/worker-boot-core.js +17 -7
- package/dist/compose-features.js +2 -3
- package/dist/run-bootstrap.d.ts +3 -1
- package/dist/run-bootstrap.js +18 -1
- package/dist/run-prod-app-boot-context.js +11 -4
- package/dist/run-prod-app.js +19 -9
- package/package.json +7 -7
- package/src/changes.json +12 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
export declare function isTransientConnectionError(error: unknown): boolean;
|
|
3
|
+
export declare const WAIT_FOR_DATABASE_INITIAL_BACKOFF_MS = 1000;
|
|
4
|
+
export declare const WAIT_FOR_DATABASE_MAX_BACKOFF_MS = 15000;
|
|
5
|
+
export declare const WAIT_FOR_DATABASE_DEADLINE_MS = 90000;
|
|
6
|
+
export type WaitForDatabaseOptions = {
|
|
7
|
+
readonly processName: string;
|
|
8
|
+
readonly log?: (line: string) => void;
|
|
9
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
10
|
+
readonly now?: () => number;
|
|
11
|
+
};
|
|
12
|
+
export declare function waitForDatabase(db: DbConnection, options: WaitForDatabaseOptions): Promise<void>;
|
|
13
|
+
export declare function waitUntilDatabaseReachable<T>(ping: () => Promise<T>, options: WaitForDatabaseOptions): Promise<T>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { extractPgError, tableExists } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
const TRANSIENT_CODES = new Set([
|
|
3
|
+
"53300", // too_many_connections
|
|
4
|
+
"57P03", // cannot_connect_now (startup / recovery)
|
|
5
|
+
"ECONNREFUSED",
|
|
6
|
+
"ECONNRESET",
|
|
7
|
+
"ETIMEDOUT",
|
|
8
|
+
"ENOTFOUND",
|
|
9
|
+
"EAI_AGAIN",
|
|
10
|
+
"CONNECTION_CLOSED",
|
|
11
|
+
"CONNECT_TIMEOUT",
|
|
12
|
+
"CONNECTION_DESTROYED",
|
|
13
|
+
"ERR_POSTGRES_CONNECTION_REFUSED", // Bun.SQL
|
|
14
|
+
"ERR_POSTGRES_CONNECTION_CLOSED", // Bun.SQL
|
|
15
|
+
"ERR_POSTGRES_CONNECTION_TIMEOUT", // Bun.SQL
|
|
16
|
+
"ERR_POSTGRES_CONNECTION_FAILED", // Bun.SQL
|
|
17
|
+
]);
|
|
18
|
+
const CONNECTION_EXCEPTION_CLASS = "08";
|
|
19
|
+
export function isTransientConnectionError(error) {
|
|
20
|
+
const code = extractPgError(error)?.code;
|
|
21
|
+
if (code === undefined)
|
|
22
|
+
return false;
|
|
23
|
+
return TRANSIENT_CODES.has(code) || code.startsWith(CONNECTION_EXCEPTION_CLASS);
|
|
24
|
+
}
|
|
25
|
+
export const WAIT_FOR_DATABASE_INITIAL_BACKOFF_MS = 1_000;
|
|
26
|
+
export const WAIT_FOR_DATABASE_MAX_BACKOFF_MS = 15_000;
|
|
27
|
+
// ponytail: the kubelet liveness probe restarts a pod without an open port after ~105s anyway,
|
|
28
|
+
// so waiting longer buys nothing. Upgrade: make this configurable via env once a deployment
|
|
29
|
+
// needs a longer outage window (e.g. a managed-DB failover that takes minutes).
|
|
30
|
+
export const WAIT_FOR_DATABASE_DEADLINE_MS = 90_000;
|
|
31
|
+
function logToConsole(line) {
|
|
32
|
+
// biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
|
|
33
|
+
console.log(line);
|
|
34
|
+
}
|
|
35
|
+
function sleepReal(ms) {
|
|
36
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
}
|
|
38
|
+
// Postgres can be briefly unreachable at boot (connection slots exhausted, failover, restart).
|
|
39
|
+
// Crashing makes the orchestrator restart-loop and adds connection churn; wait and retry instead.
|
|
40
|
+
export async function waitForDatabase(db, options) {
|
|
41
|
+
await waitUntilDatabaseReachable(() => tableExists(db, "pg_catalog.pg_class"), options);
|
|
42
|
+
}
|
|
43
|
+
export async function waitUntilDatabaseReachable(ping, options) {
|
|
44
|
+
const { processName, log = logToConsole, sleep = sleepReal, now = Date.now } = options;
|
|
45
|
+
const deadline = now() + WAIT_FOR_DATABASE_DEADLINE_MS;
|
|
46
|
+
let backoffMs = WAIT_FOR_DATABASE_INITIAL_BACKOFF_MS;
|
|
47
|
+
for (let attempt = 1;; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
return await ping();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!isTransientConnectionError(error))
|
|
53
|
+
throw error;
|
|
54
|
+
const remainingMs = deadline - now();
|
|
55
|
+
if (remainingMs <= 0)
|
|
56
|
+
throw error;
|
|
57
|
+
const waitMs = Math.min(backoffMs, remainingMs);
|
|
58
|
+
const code = extractPgError(error)?.code ?? "unknown";
|
|
59
|
+
log(`[${processName}] database not reachable (code ${code}, attempt ${attempt}), retrying in ${waitMs}ms…`);
|
|
60
|
+
await sleep(waitMs);
|
|
61
|
+
backoffMs = Math.min(backoffMs * 2, WAIT_FOR_DATABASE_MAX_BACKOFF_MS);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { loadJwtSecretOrKeyring } from "@cosmicdrift/kumiko-framework/api";
|
|
7
7
|
import { configureBlindIndexKey, configurePiiSubjectKms, resolvePlatformKeks, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
8
8
|
import { configureEntityFieldEncryption, createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
-
import { createRegistry, findTierResolverUsage,
|
|
9
|
+
import { createRegistry, findTierResolverUsage, requireTierResolverPlugin, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
10
|
import { createWorkerEntrypoint, } from "@cosmicdrift/kumiko-framework/entrypoint";
|
|
11
11
|
import { assertKumikoSchemaCurrent, SchemaDriftError, } from "@cosmicdrift/kumiko-framework/migrations";
|
|
12
12
|
import { createEntityCache, createEventDedup, createIdempotencyGuard, } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
@@ -20,6 +20,7 @@ import { requireEnv } from "../run-prod-app.js";
|
|
|
20
20
|
import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, } from "../run-prod-app-boot-context.js";
|
|
21
21
|
import { resolveBootCrypto } from "./boot-crypto.js";
|
|
22
22
|
import { jobRunLoggerCallbacks } from "./job-run-logger.js";
|
|
23
|
+
import { waitForDatabase, waitUntilDatabaseReachable } from "./wait-for-database.js";
|
|
23
24
|
import { assertWorkerMetricsOptions } from "./worker-metrics-server.js";
|
|
24
25
|
function isSecretsContext(value) {
|
|
25
26
|
if (typeof value !== "object" || value === null)
|
|
@@ -39,11 +40,7 @@ async function buildTierEffectiveFeatures(features, db, registry, cacheSync) {
|
|
|
39
40
|
const tierResolverUsage = findTierResolverUsage(features);
|
|
40
41
|
if (!tierResolverUsage)
|
|
41
42
|
return undefined;
|
|
42
|
-
|
|
43
|
-
if (!isTierResolverPlugin(plugin)) {
|
|
44
|
-
throw new Error("tier-resolver registration options must provide a build() factory");
|
|
45
|
-
}
|
|
46
|
-
return plugin.build({ db, registry, cacheSync });
|
|
43
|
+
return requireTierResolverPlugin(tierResolverUsage).build({ db, registry, cacheSync });
|
|
47
44
|
}
|
|
48
45
|
async function assertWorkerSchemaCurrent(db, migrationsDir, processName) {
|
|
49
46
|
const dir = migrationsDir ?? "./kumiko/migrations";
|
|
@@ -100,7 +97,10 @@ export async function bootWorkerProcess(options, envSource, profile) {
|
|
|
100
97
|
}
|
|
101
98
|
// Before the connections, so an abort leaks nothing.
|
|
102
99
|
if (options.kms) {
|
|
103
|
-
const
|
|
100
|
+
const kms = options.kms;
|
|
101
|
+
const kmsHealth = await waitUntilDatabaseReachable(() => kms.health(), {
|
|
102
|
+
processName: processName,
|
|
103
|
+
});
|
|
104
104
|
if (!kmsHealth.ok) {
|
|
105
105
|
throw new Error(`[${processName}] BOOT ABORTED — KMS health check failed (latency ${kmsHealth.latencyMs}ms)`);
|
|
106
106
|
}
|
|
@@ -112,6 +112,16 @@ export async function bootWorkerProcess(options, envSource, profile) {
|
|
|
112
112
|
redisUrl,
|
|
113
113
|
clientOptions: redisClientOptionsFromEnv(envSource),
|
|
114
114
|
});
|
|
115
|
+
try {
|
|
116
|
+
await waitForDatabase(db, { processName });
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
await closeDb();
|
|
120
|
+
await closeOutsideTransactionDb();
|
|
121
|
+
redis.disconnect();
|
|
122
|
+
await cacheSync.close();
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
115
125
|
const resolvedEffectiveFeatures = options.effectiveFeatures ??
|
|
116
126
|
(await buildTierEffectiveFeatures(features, db, registry, cacheSync));
|
|
117
127
|
if (options.migrations !== false) {
|
package/dist/compose-features.js
CHANGED
|
@@ -15,16 +15,15 @@
|
|
|
15
15
|
import { AUTH_SELF_REGISTRATION_FEATURE, createAuthEmailPasswordFeature, createAuthSelfRegistrationToggleFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
16
16
|
import { AUTH_MFA_FEATURE, mfaStatusCheckerFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
17
17
|
import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
18
|
-
import { createFilesTenantDataFeature } from "@cosmicdrift/kumiko-bundled-features/files-tenant-data";
|
|
18
|
+
import { createFilesTenantDataFeature, FILES_TENANT_DATA_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/files-tenant-data";
|
|
19
19
|
import { createRendererFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/renderer-foundation";
|
|
20
20
|
import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
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
24
|
import { dedupeFeatures } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
|
+
import { FILES_FEATURE } from "@cosmicdrift/kumiko-framework/files";
|
|
25
26
|
import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
|
|
26
|
-
const FILES_FEATURE = "files";
|
|
27
|
-
const FILES_TENANT_DATA_FEATURE = "files-tenant-data";
|
|
28
27
|
export function composeFeatures(appFeatures, options) {
|
|
29
28
|
// ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
|
|
30
29
|
// an app composing its own foundation and mounting auth-mfa itself is
|
package/dist/run-bootstrap.d.ts
CHANGED
|
@@ -9,5 +9,7 @@ export type RunBootstrapOptions = Omit<RunWorkerAppOptions, "wireComponents" | "
|
|
|
9
9
|
};
|
|
10
10
|
/** Returns the report, or undefined in KUMIKO_DRY_RUN_ENV=boot mode. Without
|
|
11
11
|
* an injected `envSource` (a real process) it exits the process when done,
|
|
12
|
-
* so a k8s Job never hangs on a leftover socket.
|
|
12
|
+
* so a k8s Job never hangs on a leftover socket. A SystemAdmin invite that
|
|
13
|
+
* came out `role-mismatch` or `cancelled` fails the run (exit code 1, or a
|
|
14
|
+
* thrown error with an injected `envSource`). */
|
|
13
15
|
export declare function runBootstrap(options: RunBootstrapOptions): Promise<BootstrapReport | undefined>;
|
package/dist/run-bootstrap.js
CHANGED
|
@@ -19,6 +19,12 @@ function maskEmailForLog(email) {
|
|
|
19
19
|
const at = email.indexOf("@");
|
|
20
20
|
return at <= 0 ? "***" : `${email[0]}***${email.slice(at)}`;
|
|
21
21
|
}
|
|
22
|
+
// An admin invite that can never grant its global role leaves the first deployment without
|
|
23
|
+
// a SystemAdmin; a green Job would hide that.
|
|
24
|
+
function unprovisionedGlobalRoleInvites(report) {
|
|
25
|
+
return report.invites.filter((invite) => invite.globalRoles.length > 0 &&
|
|
26
|
+
(invite.outcome === "role-mismatch" || invite.outcome === "cancelled"));
|
|
27
|
+
}
|
|
22
28
|
function logBootstrapReport(report) {
|
|
23
29
|
for (const tenant of report.tenants) {
|
|
24
30
|
// biome-ignore lint/suspicious/noConsole: the report IS the deliverable of a one-shot process
|
|
@@ -32,7 +38,9 @@ function logBootstrapReport(report) {
|
|
|
32
38
|
}
|
|
33
39
|
/** Returns the report, or undefined in KUMIKO_DRY_RUN_ENV=boot mode. Without
|
|
34
40
|
* an injected `envSource` (a real process) it exits the process when done,
|
|
35
|
-
* so a k8s Job never hangs on a leftover socket.
|
|
41
|
+
* so a k8s Job never hangs on a leftover socket. A SystemAdmin invite that
|
|
42
|
+
* came out `role-mismatch` or `cancelled` fails the run (exit code 1, or a
|
|
43
|
+
* thrown error with an injected `envSource`). */
|
|
36
44
|
export async function runBootstrap(options) {
|
|
37
45
|
const envSource = await resolveWorkerEnvSource(options, PROCESS_NAME);
|
|
38
46
|
const jwtSecret = requireEnv("JWT_SECRET", envSource, PROCESS_NAME);
|
|
@@ -68,6 +76,15 @@ export async function runBootstrap(options) {
|
|
|
68
76
|
await boot.close();
|
|
69
77
|
}
|
|
70
78
|
logBootstrapReport(report);
|
|
79
|
+
const unprovisioned = unprovisionedGlobalRoleInvites(report);
|
|
80
|
+
if (unprovisioned.length > 0) {
|
|
81
|
+
const message = `[${PROCESS_NAME}] ${unprovisioned.length} invite(s) cannot grant their global role (${unprovisioned.map((invite) => `${maskEmailForLog(invite.email)}: ${invite.outcome}`).join(", ")})`;
|
|
82
|
+
if (options.envSource !== undefined)
|
|
83
|
+
throw new Error(message);
|
|
84
|
+
// biome-ignore lint/suspicious/noConsole: the failure reason of a one-shot process
|
|
85
|
+
console.error(message);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
71
88
|
// biome-ignore lint/suspicious/noConsole: boot-time progress hint
|
|
72
89
|
console.log(`[${PROCESS_NAME}] done.`);
|
|
73
90
|
if (options.envSource === undefined) {
|
|
@@ -115,15 +115,22 @@ function createBootConfigResolver(registry, envSource, crypto) {
|
|
|
115
115
|
...(crypto.configCipher && { cipher: crypto.configCipher }),
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
+
// The delivery service reads tenant config too (tenant name/locale in mail
|
|
119
|
+
// layouts), so apps without auth still get a resolver for it; the ctx entry
|
|
120
|
+
// stays auth-only.
|
|
121
|
+
function resolveDeliveryConfigResolver(opts, hasDeliveryFeature, crypto) {
|
|
122
|
+
return opts.hasAuth || hasDeliveryFeature
|
|
123
|
+
? createBootConfigResolver(opts.registry, opts.envSource, crypto)
|
|
124
|
+
: undefined;
|
|
125
|
+
}
|
|
118
126
|
export function buildBootExtraContext(opts) {
|
|
119
127
|
const crypto = opts.crypto ?? resolveBootCrypto(opts.envSource, opts.masterKey);
|
|
120
128
|
const hasDeliveryFeature = opts.features.some((f) => f.name === DELIVERY_FEATURE);
|
|
121
129
|
const hasTenantFeature = opts.features.some((f) => f.name === TENANT_FEATURE);
|
|
122
130
|
const escapeHatchAuditSink = resolveEscapeHatchAuditSink(opts.features, opts.db);
|
|
123
131
|
const secrets = resolveBootSecrets(opts.db, opts.features, crypto, opts.registry);
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
: undefined;
|
|
132
|
+
const bootConfigResolver = resolveDeliveryConfigResolver(opts, hasDeliveryFeature, crypto);
|
|
133
|
+
const configResolver = opts.hasAuth ? bootConfigResolver : undefined;
|
|
127
134
|
return {
|
|
128
135
|
templateResolver: createTemplateResolverApi(opts.db),
|
|
129
136
|
...(opts.kms && { kms: opts.kms }),
|
|
@@ -138,7 +145,7 @@ export function buildBootExtraContext(opts) {
|
|
|
138
145
|
...(escapeHatchAuditSink && { escapeHatchAuditSink }),
|
|
139
146
|
...(opts.log && { log: opts.log }),
|
|
140
147
|
...(hasTenantFeature && { tenantUserIdsQuery: TenantQueries.resolveUserIds }),
|
|
141
|
-
configResolver,
|
|
148
|
+
configResolver: bootConfigResolver,
|
|
142
149
|
deliverQueuedInline: opts.deliverQueuedInline,
|
|
143
150
|
}),
|
|
144
151
|
}),
|
package/dist/run-prod-app.js
CHANGED
|
@@ -48,10 +48,10 @@ import { SECRETS_MASTER_KEK_ENV_KEYS } from "@cosmicdrift/kumiko-bundled-feature
|
|
|
48
48
|
import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
49
49
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
50
50
|
import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
51
|
-
import { createDefaultSseBroker, createRedisLoginRateLimiter, loadJwtSecretOrKeyring, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, } from "@cosmicdrift/kumiko-framework/api";
|
|
51
|
+
import { createDefaultSseBroker, createRedisLoginRateLimiter, loadJwtSecretOrKeyring, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, withFileProviderResolver, } from "@cosmicdrift/kumiko-framework/api";
|
|
52
52
|
import { configureBlindIndexKey, configurePiiSubjectKms, resolvePlatformKeks, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
53
53
|
import { configureEntityFieldEncryption, createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
|
|
54
|
-
import { collectWriteHandlerQns, createRegistry, findTierResolverUsage,
|
|
54
|
+
import { collectWriteHandlerQns, createRegistry, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
|
|
55
55
|
import { createAllInOneEntrypoint, createApiEntrypoint, } from "@cosmicdrift/kumiko-framework/entrypoint";
|
|
56
56
|
import { KumikoBootError, kmsSlotsOf, parseEnv, withOptionalEnvKeys, } from "@cosmicdrift/kumiko-framework/env";
|
|
57
57
|
import { renderDryRun } from "@cosmicdrift/kumiko-framework/env/dry-run";
|
|
@@ -67,6 +67,7 @@ import { resolveBootCrypto } from "./boot/boot-crypto.js";
|
|
|
67
67
|
import { jobRunLoggerCallbacks } from "./boot/job-run-logger.js";
|
|
68
68
|
import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot.js";
|
|
69
69
|
import { recordAppStartedOnBoot } from "./boot/record-app-started-on-boot.js";
|
|
70
|
+
import { waitForDatabase, waitUntilDatabaseReachable } from "./boot/wait-for-database.js";
|
|
70
71
|
import { buildBunServeOptions, resolveDerivedMaxRequestBodySize } from "./bun-serve-options.js";
|
|
71
72
|
import { buildComposeAuthOptions, composeFeatures } from "./compose-features.js";
|
|
72
73
|
import { makeDispatchSystemWrite } from "./extra-routes-deps.js";
|
|
@@ -336,7 +337,10 @@ export async function runProdApp(options) {
|
|
|
336
337
|
// against an unreachable key store — a silent start would answer every
|
|
337
338
|
// PII read with 503. Runs BEFORE connections so an abort leaks nothing.
|
|
338
339
|
if (options.kms) {
|
|
339
|
-
const
|
|
340
|
+
const kms = options.kms;
|
|
341
|
+
const kmsHealth = await waitUntilDatabaseReachable(() => kms.health(), {
|
|
342
|
+
processName: "runProdApp",
|
|
343
|
+
});
|
|
340
344
|
if (!kmsHealth.ok) {
|
|
341
345
|
throw new Error(`[runProdApp] BOOT ABORTED — KMS health check failed (latency ${kmsHealth.latencyMs}ms)`);
|
|
342
346
|
}
|
|
@@ -344,6 +348,7 @@ export async function runProdApp(options) {
|
|
|
344
348
|
const { db, close: closeDb } = createDbConnection(databaseUrl, dbConnectionOptionsFromEnv(envSource));
|
|
345
349
|
const { db: outsideTransactionDb, close: closeOutsideTransactionDb } = createDbConnection(databaseUrl, outsideTransactionPoolOptionsFromEnv(envSource));
|
|
346
350
|
const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
|
|
351
|
+
await waitForDatabase(db, { processName: "runProdApp" });
|
|
347
352
|
// Sprint-8a Tier-Composition auto-wire: scan features for a
|
|
348
353
|
// tenantTierResolver-extension. If found AND user didn't supply own
|
|
349
354
|
// effectiveFeatures, build the resolver here (db + registry are
|
|
@@ -359,11 +364,11 @@ export async function runProdApp(options) {
|
|
|
359
364
|
if (resolvedEffectiveFeatures === undefined) {
|
|
360
365
|
const tierResolverUsage = findTierResolverUsage(features);
|
|
361
366
|
if (tierResolverUsage) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
+
resolvedEffectiveFeatures = await requireTierResolverPlugin(tierResolverUsage).build({
|
|
368
|
+
db,
|
|
369
|
+
registry,
|
|
370
|
+
cacheSync,
|
|
371
|
+
});
|
|
367
372
|
}
|
|
368
373
|
}
|
|
369
374
|
// 5. Schema-Drift-Gate (drizzle-frei, kumiko/migrations). `kumiko schema
|
|
@@ -469,6 +474,11 @@ export async function runProdApp(options) {
|
|
|
469
474
|
// über die auth-routes.
|
|
470
475
|
// Secure-by-default (#1372): sessionStore provider → resolveSessionStore.
|
|
471
476
|
const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
|
|
477
|
+
if (mfaFeature && registry.getWriteHandler(AuthHandlers.switchTenantMfaGate) === undefined) {
|
|
478
|
+
throw new Error(`[runProdApp] auth-mfa is mounted but ${AuthHandlers.switchTenantMfaGate} is not registered: ` +
|
|
479
|
+
"createAuthEmailPasswordFeature registers it only when mfaStatusChecker is set, and without it " +
|
|
480
|
+
"POST /auth/switch-tenant would mint sessions without the second factor");
|
|
481
|
+
}
|
|
472
482
|
// Hoisted above buildProdSessionAuth: that call also wires MFA-enable/
|
|
473
483
|
// disable → PAT-revoke (bindRevokeAllPatTokensFromFeature), so it needs
|
|
474
484
|
// patFeature too.
|
|
@@ -692,7 +702,7 @@ export async function runProdApp(options) {
|
|
|
692
702
|
...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
|
|
693
703
|
});
|
|
694
704
|
const seedDispatcher = (options.seeds?.length ?? 0) > 0 || options.seedsDir !== undefined
|
|
695
|
-
? createDispatcher(registry, { db, redis, entityCache, registry, ...extraContext })
|
|
705
|
+
? createDispatcher(registry, withFileProviderResolver(registry, { db, redis, entityCache, registry, ...extraContext }, cacheSync))
|
|
696
706
|
: undefined;
|
|
697
707
|
if (seedDispatcher) {
|
|
698
708
|
for (const seed of options.seeds ?? []) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-server-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.1",
|
|
4
4
|
"description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -84,12 +84,12 @@
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
88
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
89
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
90
|
-
"@cosmicdrift/kumiko-renderer-mail-html": "0.
|
|
91
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
92
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
87
|
+
"@cosmicdrift/kumiko-bundled-features": "0.365.1",
|
|
88
|
+
"@cosmicdrift/kumiko-framework": "0.365.1",
|
|
89
|
+
"@cosmicdrift/kumiko-headless": "0.365.1",
|
|
90
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "0.365.1",
|
|
91
|
+
"@cosmicdrift/kumiko-renderer-web": "0.365.1",
|
|
92
|
+
"@cosmicdrift/kumiko-types": "0.365.1",
|
|
93
93
|
"bun-types": "^1.3.13",
|
|
94
94
|
"ioredis": "^5.10.1",
|
|
95
95
|
"temporal-polyfill": "^0.3.2"
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Bootstrap fails when a global-role invite cannot be provisioned; delivery notify works without auth",
|
|
6
|
+
"detail": "runBootstrap now fails with a non-zero exit when an invite with global roles ends role-mismatch or cancelled. The delivery notify factory gets a config resolver also when the app has no auth feature."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Seeds can create fileRefs through handlers that use createFileRefFromBytes",
|
|
12
|
+
"detail": "The seed dispatcher built its handler context without the file provider resolver, so a write handler invoked via systemWriteAs failed with \"createFileRefFromBytes: ctx.files is unavailable\". The seed context now carries the same file provider as the HTTP path."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.363.0",
|
|
4
16
|
"type": "improvement",
|