@cosmicdrift/kumiko-server-runtime 0.410.0 → 0.411.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.
@@ -22,6 +22,7 @@ export declare function buildEffectiveFeaturesResolver(deps: {
22
22
  readonly registry: Registry;
23
23
  readonly cacheSync: CacheSyncBus;
24
24
  }): Promise<EffectiveFeaturesResolver | undefined>;
25
+ export declare const BOOT_SEEDS_LOCK_KEY = 1651729764;
25
26
  export declare function applyStandardBootSeeds(deps: {
26
27
  readonly db: DbConnection;
27
28
  readonly registry: Registry;
@@ -5,7 +5,7 @@ import { AuthErrors, AuthHandlers, AuthQueries, } from "@cosmicdrift/kumiko-bund
5
5
  import { seedAdminGuarded, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
6
6
  import { AuthMfaHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
7
7
  import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
8
- import { createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, serverTimeoutOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
8
+ import { createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, serverTimeoutOptionsFromEnv, withSessionAdvisoryLock, } from "@cosmicdrift/kumiko-framework/db";
9
9
  import { collectWriteHandlerQns, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
10
10
  import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
11
11
  import { buildComposeAuthOptions, composeFeatures, } from "../compose-features.js";
@@ -46,12 +46,16 @@ export async function buildEffectiveFeaturesResolver(deps) {
46
46
  const bound = await featureToggles.bind({ db, registry, cacheSync, tierResolver });
47
47
  return bound.resolver;
48
48
  }
49
+ export const BOOT_SEEDS_LOCK_KEY = 0x62_73_65_64; // 'bsed'
49
50
  export async function applyStandardBootSeeds(deps) {
50
51
  const { db, registry, admin, shortcutSeeds, cipher, processName, databaseWait } = deps;
51
52
  let adminUserId;
52
53
  // Seeds are idempotent, so a transient connection error re-runs the whole sequence; any other
53
54
  // error stays fatal. Later boot steps build on the seeds, so this cannot continue in the background.
54
- await waitUntilDatabaseReachable(async () => {
55
+ await waitUntilDatabaseReachable(
56
+ // Serialised across instances: seedUser is check-then-insert, so two booting replicas could
57
+ // otherwise both insert the same admin.
58
+ () => withSessionAdvisoryLock(db, BOOT_SEEDS_LOCK_KEY, async () => {
55
59
  if (admin) {
56
60
  // Operator-created account, not a self-registration: under emailVerificationMode "strict"
57
61
  // an unverified seed admin could never log in. `admin.emailVerified` still overrides.
@@ -66,7 +70,7 @@ export async function applyStandardBootSeeds(deps) {
66
70
  db,
67
71
  ...(cipher && { cipher }),
68
72
  });
69
- }, { ...databaseWait, processName, target: "database (boot seeds)" });
73
+ }), { ...databaseWait, processName, target: "database (boot seeds)" });
70
74
  return adminUserId === undefined ? {} : { adminUserId };
71
75
  }
72
76
  const MFA_ROUTE_HANDLERS = {
@@ -1,7 +1,7 @@
1
1
  import { smtpEnvSchema } from "@cosmicdrift/kumiko-bundled-features/channel-email";
2
2
  import { kmsEnvSchema } from "@cosmicdrift/kumiko-bundled-features/crypto-shredding/env";
3
3
  import { createS3EnvSchema, s3EnvSchema, } from "@cosmicdrift/kumiko-bundled-features/file-provider-s3";
4
- import { demoAdminEnvSchema } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
4
+ import { bootstrapAdminEnvSchema, demoAdminEnvSchema, } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
5
5
  import { subscriptionStripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
6
6
  import { readKumikoMeta } from "@cosmicdrift/kumiko-framework/env";
7
7
  import * as z from "zod";
@@ -31,17 +31,16 @@ function pickShape(shape, keys) {
31
31
  }
32
32
  return picked;
33
33
  }
34
- function sysadminTenantNeedsEnvId(seeds) {
35
- const { sysadminFromEnv } = seeds;
36
- return (typeof sysadminFromEnv === "object" &&
37
- sysadminFromEnv.tenant !== undefined &&
38
- sysadminFromEnv.tenant.id === undefined);
34
+ function accountTenantNeedsEnvId(account) {
35
+ return (typeof account === "object" && account.tenant !== undefined && account.tenant.id === undefined);
39
36
  }
40
37
  function seedEnvBlock(seeds) {
41
38
  const keys = [];
42
39
  const required = [];
43
40
  const demoTenantNeedsEnvId = seeds.demoTenant !== undefined && seeds.demoTenant.id === undefined;
44
- if (demoTenantNeedsEnvId || sysadminTenantNeedsEnvId(seeds)) {
41
+ if (demoTenantNeedsEnvId ||
42
+ accountTenantNeedsEnvId(seeds.sysadminFromEnv) ||
43
+ accountTenantNeedsEnvId(seeds.bootstrapAdminFromEnv)) {
45
44
  keys.push("DEMO_TENANT_ID");
46
45
  required.push("DEMO_TENANT_ID");
47
46
  }
@@ -53,9 +52,14 @@ function seedEnvBlock(seeds) {
53
52
  keys.push("DEMO_SYSADMIN_EMAIL", "DEMO_SYSADMIN_PASSWORD");
54
53
  required.push("DEMO_SYSADMIN_EMAIL", "DEMO_SYSADMIN_PASSWORD");
55
54
  }
56
- if (keys.length === 0)
55
+ const bootstrapShape = seeds.bootstrapAdminFromEnv ? bootstrapAdminEnvSchema.shape : {};
56
+ if (keys.length === 0 && !seeds.bootstrapAdminFromEnv)
57
57
  return undefined;
58
- return { source: "runProdApp.seeds", shape: pickShape(demoAdminEnvSchema.shape, keys), required };
58
+ return {
59
+ source: "runProdApp.seeds",
60
+ shape: { ...pickShape(demoAdminEnvSchema.shape, keys), ...bootstrapShape },
61
+ required,
62
+ };
59
63
  }
60
64
  function fileStorageEnvBlocks(fileStorage) {
61
65
  const blocks = [];
@@ -27,6 +27,7 @@ export type BootOptionFields = {
27
27
  readonly appOverrides?: object;
28
28
  readonly auth?: {
29
29
  readonly admin?: unknown;
30
+ readonly passwordReset?: unknown;
30
31
  readonly mail?: {
31
32
  readonly from?: string;
32
33
  };
@@ -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 { BOOTSTRAP_ADMIN_EMAIL_ENV } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
4
5
  import { SUBSCRIPTION_PADDLE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle";
5
6
  import { SUBSCRIPTION_STRIPE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
6
7
  import { TIER_ENGINE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
@@ -43,8 +44,20 @@ function wireFeatureToggles(options, mounted) {
43
44
  }
44
45
  return undefined;
45
46
  }
46
- function warnAboutBootOptions(options) {
47
+ function bootstrapAdminCannotSetPassword(options, envSource) {
48
+ const { seeds } = options;
49
+ return (seeds !== undefined &&
50
+ !isSeedFnList(seeds) &&
51
+ Boolean(seeds.bootstrapAdminFromEnv) &&
52
+ Boolean(envSource[BOOTSTRAP_ADMIN_EMAIL_ENV]) &&
53
+ options.auth?.passwordReset === undefined &&
54
+ options.auth?.mail === undefined);
55
+ }
56
+ function warnAboutBootOptions(options, envSource) {
47
57
  const bootWarnings = stackBootWarnings(options).map((line) => `stacks in \`features\` expect boot options that are not set:\n${line}`);
58
+ if (bootstrapAdminCannotSetPassword(options, envSource)) {
59
+ bootWarnings.push(`${BOOTSTRAP_ADMIN_EMAIL_ENV} is set but neither auth.passwordReset nor auth.mail is configured: the bootstrap admin has no password and could never set one.`);
60
+ }
48
61
  if (options.runSingleInstance === false &&
49
62
  (options.mail || options.fileStorage || options.featureToggles || options.appOverrides) &&
50
63
  options.workerMountsFeatures !== true) {
@@ -101,7 +114,7 @@ export async function resolveBootOptionWiring(options, envSource) {
101
114
  if (demoMode) {
102
115
  extraRoutes.push(buildDemoModeRoute(demoMode, envSource));
103
116
  }
104
- warnAboutBootOptions(options);
117
+ warnAboutBootOptions(options, envSource);
105
118
  const seeds = options.seeds ?? [];
106
119
  const shortcutSeeds = isSeedFnList(seeds)
107
120
  ? []
@@ -20,6 +20,10 @@ type SeededAccountOptions = {
20
20
  * - `sysadminFromEnv`: `DEMO_SYSADMIN_EMAIL` / `DEMO_SYSADMIN_PASSWORD`, global
21
21
  * role `SystemAdmin`, email verified, member of `tenant` (default
22
22
  * `demoTenant`) with role `User` (default).
23
+ * - `bootstrapAdminFromEnv`: `KUMIKO_BOOTSTRAP_ADMIN_EMAIL` (no seed while unset) becomes an
24
+ * admin WITHOUT a password in `tenant` (default `demoTenant`) with `Admin` + `TenantAdmin`
25
+ * and the global role `SystemAdmin`; it sets its password via password reset. One-shot:
26
+ * skipped when the account already has a password or the tenant already has another member.
23
27
  * - `run`: free seed functions, same contract as the array form.
24
28
  *
25
29
  * @example
@@ -36,6 +40,10 @@ export type ProdSeedsConfig = {
36
40
  readonly sysadminFromEnv?: boolean | (SeededAccountOptions & {
37
41
  readonly tenant?: ProdSeedTenant;
38
42
  });
43
+ readonly bootstrapAdminFromEnv?: boolean | (SeededAccountOptions & {
44
+ readonly tenant?: ProdSeedTenant;
45
+ readonly globalRoles?: readonly string[];
46
+ });
39
47
  readonly run?: readonly ProdSeedFn[];
40
48
  };
41
49
  export type DbSeedFn = (db: DbConnection) => Promise<void>;
@@ -1,4 +1,5 @@
1
- import { seedAdminGuarded, seedTenant, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
1
+ import { seedAdminGuarded, seedBootstrapAdmin, seedTenant, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
2
+ import { BOOTSTRAP_ADMIN_EMAIL_ENV } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
2
3
  import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
3
4
  import { parseTenantId } from "@cosmicdrift/kumiko-types/identifiers";
4
5
  const DEFAULT_DEMO_ADMIN_ROLES = ["Admin", ROLES.TenantAdmin];
@@ -65,12 +66,36 @@ function buildSysadminSeed(value, demoTenant, envSource) {
65
66
  memberships: [membershipOf(tenant, options.roles ?? DEFAULT_SYSADMIN_TENANT_ROLES)],
66
67
  });
67
68
  }
69
+ function buildBootstrapAdminSeed(value, demoTenant, envSource) {
70
+ const options = typeof value === "object" ? value : {};
71
+ const tenant = options.tenant
72
+ ? resolveTenant(options.tenant, envSource, "bootstrapAdminFromEnv.tenant")
73
+ : demoTenant;
74
+ if (!tenant) {
75
+ throw new Error("[runProdApp] seeds.bootstrapAdminFromEnv needs seeds.demoTenant or bootstrapAdminFromEnv.tenant for its membership.");
76
+ }
77
+ const email = envSource[BOOTSTRAP_ADMIN_EMAIL_ENV];
78
+ if (!email)
79
+ return undefined;
80
+ const admin = {
81
+ email,
82
+ displayName: options.displayName ?? "Admin",
83
+ globalRoles: options.globalRoles ?? [ROLES.SystemAdmin],
84
+ emailVerified: options.emailVerified ?? true,
85
+ memberships: [membershipOf(tenant, options.roles ?? DEFAULT_DEMO_ADMIN_ROLES)],
86
+ };
87
+ return async (db) => {
88
+ await seedBootstrapAdmin(db, admin);
89
+ };
90
+ }
68
91
  /** Validates env and options at boot (before any connection) and returns the
69
92
  * seeds to run once the database is up. */
70
93
  export function resolveSeedShortcuts(seeds, deps) {
71
- const wantsAccounts = Boolean(seeds.demoAdminFromEnv) || Boolean(seeds.sysadminFromEnv);
94
+ const wantsAccounts = Boolean(seeds.demoAdminFromEnv) ||
95
+ Boolean(seeds.sysadminFromEnv) ||
96
+ Boolean(seeds.bootstrapAdminFromEnv);
72
97
  if ((wantsAccounts || seeds.demoTenant) && !deps.hasAuth) {
73
- throw new Error("[runProdApp] seeds.demoTenant / demoAdminFromEnv / sysadminFromEnv need `auth` (user, tenant and auth-email-password features).");
98
+ throw new Error("[runProdApp] seeds.demoTenant / demoAdminFromEnv / sysadminFromEnv / bootstrapAdminFromEnv need `auth` (user, tenant and auth-email-password features).");
74
99
  }
75
100
  if (seeds.demoAdminFromEnv && deps.hasAuthAdmin) {
76
101
  throw new Error("[runProdApp] seeds.demoAdminFromEnv and auth.admin both seed the admin — keep one.");
@@ -90,5 +115,10 @@ export function resolveSeedShortcuts(seeds, deps) {
90
115
  if (seeds.sysadminFromEnv) {
91
116
  result.push(buildSysadminSeed(seeds.sysadminFromEnv, demoTenant, deps.envSource));
92
117
  }
118
+ if (seeds.bootstrapAdminFromEnv) {
119
+ const bootstrapSeed = buildBootstrapAdminSeed(seeds.bootstrapAdminFromEnv, demoTenant, deps.envSource);
120
+ if (bootstrapSeed)
121
+ result.push(bootstrapSeed);
122
+ }
93
123
  return result;
94
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-server-runtime",
3
- "version": "0.410.0",
3
+ "version": "0.411.0",
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 tooling.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -96,11 +96,11 @@
96
96
  }
97
97
  },
98
98
  "dependencies": {
99
- "@cosmicdrift/kumiko-bundled-features": "0.410.0",
100
- "@cosmicdrift/kumiko-framework": "0.410.0",
101
- "@cosmicdrift/kumiko-renderer": "0.410.0",
102
- "@cosmicdrift/kumiko-renderer-mail-html": "0.410.0",
103
- "@cosmicdrift/kumiko-types": "0.410.0",
99
+ "@cosmicdrift/kumiko-bundled-features": "0.411.0",
100
+ "@cosmicdrift/kumiko-framework": "0.411.0",
101
+ "@cosmicdrift/kumiko-renderer": "0.411.0",
102
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.411.0",
103
+ "@cosmicdrift/kumiko-types": "0.411.0",
104
104
  "ioredis": "^5.11.1",
105
105
  "temporal-polyfill": "^0.3.2",
106
106
  "zod": "^4.6.7"
@@ -109,7 +109,7 @@
109
109
  "bun-types": "^1.4.4"
110
110
  },
111
111
  "devDependencies": {
112
- "@cosmicdrift/kumiko-testing": "0.410.0",
112
+ "@cosmicdrift/kumiko-testing": "0.411.0",
113
113
  "bun-types": "^1.4.4"
114
114
  },
115
115
  "publishConfig": {
package/src/changes.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.411.0",
4
+ "type": "improvement",
5
+ "title": "seeds.bootstrapAdminFromEnv creates the app creator as admin",
6
+ "detail": "`runProdApp({ seeds: { bootstrapAdminFromEnv: true | { tenant, displayName, roles, globalRoles, emailVerified } } })` reads `KUMIKO_BOOTSTRAP_ADMIN_EMAIL` (optional, validated as an email in the env schema) and idempotently creates that account without a password, with the Admin and TenantAdmin roles in the tenant (`seeds.demoTenant` or the option's `tenant`), the SystemAdmin global role and a verified email. An empty variable seeds nothing. An existing account that already has a password (for example a self-registration with the same email) is left untouched: no verification, no membership, no role change; boot logs one warning without the email. It works next to `auth.admin`. Boot warns when the variable is set but neither `auth.passwordReset` nor `auth.mail` is configured, because the admin could never set a password."
7
+ },
8
+ {
9
+ "version": "0.411.0",
10
+ "type": "fix",
11
+ "title": "Boot seeds run under an advisory lock",
12
+ "detail": "`applyStandardBootSeeds` holds a session advisory lock (`BOOT_SEEDS_LOCK_KEY`) while it seeds, for `runProdApp` and `runDevApp` alike. Two pods starting at the same time no longer race on add-only seeds such as the demo admin. The lock takes one pooled connection while the seeds run."
13
+ },
2
14
  {
3
15
  "version": "0.409.0",
4
16
  "type": "improvement",