@cosmicdrift/kumiko-dev-server 0.157.2 → 0.159.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.157.2",
3
+ "version": "0.159.1",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Not shipped into production node_modules — see @cosmicdrift/kumiko-server-runtime for the prod boot path.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -54,9 +54,9 @@
54
54
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
55
55
  },
56
56
  "dependencies": {
57
- "@cosmicdrift/kumiko-bundled-features": "0.157.2",
58
- "@cosmicdrift/kumiko-framework": "0.157.2",
59
- "@cosmicdrift/kumiko-server-runtime": "0.157.2",
57
+ "@cosmicdrift/kumiko-bundled-features": "0.159.1",
58
+ "@cosmicdrift/kumiko-framework": "0.159.1",
59
+ "@cosmicdrift/kumiko-server-runtime": "0.159.1",
60
60
  "ts-morph": "^28.0.0"
61
61
  },
62
62
  "publishConfig": {
@@ -1,4 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
3
+ import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
2
4
  import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
3
5
  import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
4
6
  import {
@@ -110,9 +112,15 @@ describe("composeStacks", () => {
110
112
  expect(names.filter((n) => n === "sessions")).toHaveLength(2);
111
113
  expect(() =>
112
114
  validateBoot(
113
- composeFeatures([...composeIdentityStack(), ...composeGdprStack({ sessions: true })], {
114
- includeBundled: true,
115
- }),
115
+ composeFeatures(
116
+ [
117
+ authFoundationFeature,
118
+ createPersonalAccessTokensFeature({ scopes: {} }),
119
+ ...composeIdentityStack(),
120
+ ...composeGdprStack({ sessions: true }),
121
+ ],
122
+ { includeBundled: true },
123
+ ),
116
124
  ),
117
125
  ).toThrow(/duplicate feature/i);
118
126
  });
@@ -123,6 +131,8 @@ describe("composeStacks boots for real", () => {
123
131
  test("studio-shaped combined stack passes validateBoot (not just name-list comparison)", () => {
124
132
  const features = composeFeatures(
125
133
  [
134
+ authFoundationFeature,
135
+ createPersonalAccessTokensFeature({ scopes: {} }),
126
136
  ...composeOpsStack({ rateLimiting: true }),
127
137
  ...composePagesStack(),
128
138
  ...composeMailStack({ transports: ["inmemory"] }),
@@ -12,6 +12,7 @@ import {
12
12
  seedAdmin,
13
13
  seedUserWithPassword,
14
14
  } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
15
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
15
16
  import {
16
17
  AuthMfaHandlers,
17
18
  base32Decode,
@@ -94,6 +95,13 @@ async function markVerified(userId: string): Promise<void> {
94
95
  await updateRows(stack.db, userTable, { emailVerified: true }, { id: userId });
95
96
  }
96
97
 
98
+ // writeOk drives real HTTP through the auth middleware, so a hand-built
99
+ // actor needs a live sid — sessionChecker rejects sidless JWTs.
100
+ async function withSession(user: SessionUser): Promise<SessionUser> {
101
+ const sid = await callbacks.get().sessionCreator(user, { ip: "127.0.0.1", userAgent: "test" });
102
+ return { ...user, sid };
103
+ }
104
+
97
105
  beforeAll(async () => {
98
106
  configureEntityFieldEncryption(createTestEnvelopeCipher());
99
107
  const bound = sessionCallbacksFromLateBound(callbacks);
@@ -110,6 +118,7 @@ beforeAll(async () => {
110
118
 
111
119
  const features = composeFeatures(
112
120
  [
121
+ authFoundationFeature,
113
122
  ...identity,
114
123
  ...composeOpsStack({ delivery: true, audit: false, jobs: false }),
115
124
  ...composeRendererStack(),
@@ -147,8 +156,8 @@ beforeAll(async () => {
147
156
  }),
148
157
  authConfig: {
149
158
  ...bound.asAuthConfig(),
150
- // No sessionStrictMode: seed/writeOk uses TestUsers JWTs without sid.
151
- // Login still gets jti via sessionCreator (asserted below).
159
+ // sessionChecker rejects sidless JWTs hand-built actors used with
160
+ // writeOk go through withSession() to get a live sid first.
152
161
  membershipQuery: "tenant:query:memberships",
153
162
  loginHandler: AuthHandlers.login,
154
163
  mfaVerifyHandler: AuthMfaHandlers.verify,
@@ -293,7 +302,7 @@ describe("saas-identity-wire", () => {
293
302
  });
294
303
  await markVerified(adminId);
295
304
 
296
- const admin: SessionUser = { id: adminId, tenantId, roles: ["Admin"] };
305
+ const admin = await withSession({ id: adminId, tenantId, roles: ["Admin"] });
297
306
  const invitee = "carol-wire@example.com";
298
307
  await stack.http.writeOk(AuthHandlers.inviteCreate, { email: invitee, role: "User" }, admin);
299
308
  expect(emailTransport.sent.length).toBeGreaterThanOrEqual(1);
@@ -368,7 +377,7 @@ describe("saas-identity-wire", () => {
368
377
  ],
369
378
  });
370
379
  await markVerified(userId);
371
- const user: SessionUser = { id: userId, tenantId, roles: ["User"] };
380
+ const user = await withSession({ id: userId, tenantId, roles: ["User"] });
372
381
 
373
382
  const start = await stack.http.writeOk<{ setupToken: string; otpauthUri: string }>(
374
383
  AuthMfaHandlers.enableStart,
@@ -92,6 +92,7 @@ describe("scaffoldApp", () => {
92
92
 
93
93
  const kumikoBin = readFileSync(join(dest, "bin/kumiko.ts"), "utf-8");
94
94
  expect(kumikoBin).toContain("runSchemaCli");
95
+ expect(kumikoBin).toContain("runConsumerCli");
95
96
  expect(kumikoBin).toContain("includeBundled: HAS_AUTH");
96
97
  });
97
98
 
@@ -13,6 +13,8 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
13
13
  import { mkdtempSync, readFileSync, rmSync } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
17
+ import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
16
18
  import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
17
19
  import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
18
20
  import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
@@ -77,18 +79,32 @@ describe("walkthrough — DX-3.1 snapshot", () => {
77
79
  // a dummy defineFeature here — the scaffold-side of "notesFeature"
78
80
  // (file-content) is pinned in test 2; this test pins the runtime-side
79
81
  // (composeFeatures auto-prepend behaviour the walkthrough claims).
82
+ //
83
+ // sessions now requires auth-foundation (#1370/#1371), which itself
84
+ // needs a tokenVerifier provider mounted (#1368) — PAT here, mirroring
85
+ // the scaffold picker's real-world default. Not part of the walkthrough's
86
+ // advertised 7-feature count (that count is about includeBundled's
87
+ // auto-mount, unrelated to this coupling), so both are added on top.
80
88
  const notesFeature = defineFeature("notes", () => {});
81
- const APP_FEATURES = [createSecretsFeature(), createSessionsFeature(), notesFeature];
89
+ const APP_FEATURES = [
90
+ createSecretsFeature(),
91
+ authFoundationFeature,
92
+ createPersonalAccessTokensFeature({ scopes: {} }),
93
+ createSessionsFeature(),
94
+ notesFeature,
95
+ ];
82
96
 
83
97
  const composed = composeFeatures(APP_FEATURES, { includeBundled: true });
84
- // 3 explicit + 4 auto-mounted bundled = 7 total features.
85
- expect(composed.length).toBe(7);
98
+ // 5 explicit + 4 auto-mounted bundled = 9 total features.
99
+ expect(composed.length).toBe(9);
86
100
 
87
101
  const composedNames = composed.map((f) => f.name).sort();
88
102
  expect(composedNames).toEqual([
89
103
  "auth-email-password",
104
+ "auth-foundation",
90
105
  "config",
91
106
  "notes",
107
+ "personal-access-tokens",
92
108
  "secrets",
93
109
  "sessions",
94
110
  "tenant",
@@ -97,9 +113,9 @@ describe("walkthrough — DX-3.1 snapshot", () => {
97
113
 
98
114
  // validateBoot must pass (no missing-requires, no schema-errors).
99
115
  expect(() => validateBoot(composed)).not.toThrow();
100
- // Registry must contain all 7 features.
116
+ // Registry must contain all 9 features.
101
117
  const registry = createRegistry(composed);
102
- expect(registry.features.size).toBe(7);
118
+ expect(registry.features.size).toBe(9);
103
119
  });
104
120
 
105
121
  test("bin/main.ts contains the auth.admin stub the walkthrough relies on", async () => {
@@ -18,6 +18,7 @@ import {
18
18
  type SeedAdminOptions,
19
19
  seedAdmin,
20
20
  } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
21
+ import { resolveTokenVerifier } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
21
22
  import {
22
23
  AUTH_MFA_FEATURE,
23
24
  AuthMfaHandlers,
@@ -28,10 +29,8 @@ import {
28
29
  createConfigResolver,
29
30
  } from "@cosmicdrift/kumiko-bundled-features/config";
30
31
  import {
31
- createPatResolver,
32
32
  PAT_FEATURE,
33
33
  patRateLimitFromFeature,
34
- patScopesFromFeature,
35
34
  } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
36
35
  import {
37
36
  bindAutoRevokeFromFeature,
@@ -44,7 +43,7 @@ import {
44
43
  resolveTenantLifecycleGate,
45
44
  TENANT_LIFECYCLE_FEATURE,
46
45
  } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
47
- import type { PatResolver, SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
46
+ import type { SessionMetadata, TokenVerifier } from "@cosmicdrift/kumiko-framework/api";
48
47
  import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
49
48
  import {
50
49
  configureBlindIndexKey,
@@ -89,6 +88,7 @@ import { renderWelcomeBanner } from "./welcome-banner";
89
88
  // @cosmicdrift/kumiko-server-runtime (single source of truth) — hier nur
90
89
  // durchgereicht.
91
90
  export type {
91
+ AccountUnlockSetup,
92
92
  AuthMailOptions,
93
93
  EmailVerificationSetup,
94
94
  InviteSetup,
@@ -97,6 +97,7 @@ export type {
97
97
  } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
98
98
 
99
99
  import type {
100
+ AccountUnlockSetup,
100
101
  AuthMailOptions,
101
102
  EmailVerificationSetup,
102
103
  InviteSetup,
@@ -120,7 +121,7 @@ export type RunDevAppAuthOptions = {
120
121
  /** Opt-in: revocable server-side sessions. Caller MUSS
121
122
  * `createSessionsFeature()` zu `features` adden — runDevApp wired
122
123
  * hier nur die Auth-Callbacks (creator/revoker/checker) gegen
123
- * stack.db, plus sessionStrictMode=true.
124
+ * stack.db (sidless JWTs werden dann abgelehnt).
124
125
  *
125
126
  * Standardverhalten ohne diese Option: stateless JWTs ohne sid,
126
127
  * Logout ist client-side cookie-clear, Karten­haus existing-Apps
@@ -145,6 +146,8 @@ export type RunDevAppAuthOptions = {
145
146
  readonly signup?: SignupSetup;
146
147
  /** Tenant-Invite flow (Magic-Link). Symmetric. */
147
148
  readonly invite?: InviteSetup;
149
+ /** Account-unlock flow (#1266). Symmetric zu RunProdAppAuthOptions. */
150
+ readonly accountUnlock?: AccountUnlockSetup;
148
151
  /** Domain attribute for both auth cookies (see
149
152
  * AuthRoutesConfig.cookieDomain). Symmetric zu RunProdAppAuthOptions. */
150
153
  readonly cookieDomain?: string;
@@ -406,21 +409,21 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
406
409
  }
407
410
  return sessionCallbacks;
408
411
  };
409
- // PAT opt-in: same late-bound holder pattern — the resolver needs the real
410
- // db (only concrete after setupTestStack). Wired when the feature is mounted;
411
- // scopes come from the feature's exports (single source with its handlers).
412
- let patResolver: PatResolver | undefined;
412
+ // Token-verifier opt-in: same late-bound holder pattern — resolveTokenVerifier
413
+ // needs the real db+registry (only concrete after setupTestStack). Wired
414
+ // whenever a provider feature (personal-access-tokens today) is mounted.
415
+ let tokenVerifier: TokenVerifier | undefined;
413
416
  let lifecycleDb: DbConnection | undefined;
414
417
  const patFeature = features.find((f) => f.name === PAT_FEATURE);
415
418
  const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
416
419
  const tenantLifecycleFeature = features.find((f) => f.name === TENANT_LIFECYCLE_FEATURE);
417
420
  const patAuthFragment = patFeature
418
421
  ? {
419
- patResolver: (rawToken: string) => {
420
- if (!patResolver) {
421
- throw new Error("[runDevApp] pat-resolver accessed before onAfterSetup");
422
+ tokenVerifier: (rawToken: string) => {
423
+ if (!tokenVerifier) {
424
+ throw new Error("[runDevApp] token-verifier accessed before onAfterSetup");
422
425
  }
423
- return patResolver(rawToken);
426
+ return tokenVerifier(rawToken);
424
427
  },
425
428
  patRateLimiter: (() => {
426
429
  const rl = patRateLimitFromFeature(patFeature);
@@ -449,11 +452,6 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
449
452
  sessionRevoker: (sid: string) => requireSessions().sessionRevoker(sid),
450
453
  sessionChecker: (sid: string, userId: string) =>
451
454
  requireSessions().sessionChecker(sid, userId),
452
- // strict-mode: jede neue Plattform-App startet ohne legacy-
453
- // JWTs ohne sid, daher safe als Default. Wer Sessions opt-in
454
- // wählt, will explizite Server-side Revocation — strict-mode
455
- // ist der einzige Modus der das tatsächlich erzwingt.
456
- sessionStrictMode: true,
457
455
  }
458
456
  : {};
459
457
 
@@ -509,6 +507,12 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
509
507
  confirmHandler: AuthHandlers.verifyEmail,
510
508
  },
511
509
  }),
510
+ ...(effectiveAuth.accountUnlock && {
511
+ accountUnlock: {
512
+ requestHandler: AuthHandlers.requestAccountUnlock,
513
+ confirmHandler: AuthHandlers.confirmAccountUnlock,
514
+ },
515
+ }),
512
516
  ...(effectiveAuth.signup && {
513
517
  signup: {
514
518
  requestHandler: AuthHandlers.signupRequest,
@@ -558,7 +562,8 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
558
562
  }
559
563
  }
560
564
  if (patFeature) {
561
- patResolver = createPatResolver({ db: stack.db, scopes: patScopesFromFeature(patFeature) });
565
+ tokenVerifier = (rawToken) =>
566
+ resolveTokenVerifier({ db: stack.db, registry: stack.registry }, rawToken);
562
567
  }
563
568
  if (effectiveAuth) {
564
569
  await seedAdmin(stack.db, effectiveAuth.admin);
@@ -765,7 +765,7 @@ deploys. Build context = app repo root; migrations ship in \`kumiko/migrations/\
765
765
  - \`kumiko/schema.ts\` — same feature set → \`ENTITY_METAS\` for \`kumiko schema\`.
766
766
  - \`bin/dev.ts\` — dev-server entry (\`bun dev\`).
767
767
  - \`bin/main.ts\` — production-bootstrap (\`bun run start\`).
768
- - \`bin/kumiko.ts\` — schema-CLI bundled into \`dist-server/kumiko.js\`.
768
+ - \`bin/kumiko.ts\` — schema + consumer-ops CLI bundled into \`dist-server/kumiko.js\`.
769
769
  - \`docker-compose.yml\` — local Postgres + Redis for \`bun dev\`.
770
770
 
771
771
  For full docs see https://docs.kumiko.rocks.
@@ -806,25 +806,32 @@ function renderBinKumiko(): string {
806
806
  return [
807
807
  "#!/usr/bin/env bun",
808
808
  "",
809
- "// Standalone kumiko schema-CLI for the production bundle. The deploy",
810
- "// migrate-step runs `bun /app/kumiko.js schema apply`; kumiko-build bundles",
811
- "// this file to dist-server/kumiko.js.",
809
+ "// Standalone kumiko CLI for the production bundle. The deploy migrate-step",
810
+ "// runs `bun /app/kumiko.js schema apply`; ops runs `bun /app/kumiko.js",
811
+ "// consumer status|restart <name>` to recover a dead event consumer without",
812
+ "// ad-hoc SQL. kumiko-build bundles this file to dist-server/kumiko.js.",
812
813
  "",
813
814
  'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
815
+ 'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
814
816
  'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
815
817
  'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
816
818
  "",
817
819
  "const [, , cmd, ...rest] = Bun.argv;",
818
- 'if (cmd !== "schema") {',
819
- " // biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
820
- ' console.error("\\n Unknown: kumiko " + (cmd ?? "") + " — only \'kumiko schema <sub>\' in the standalone bundle.\\n");',
821
- " process.exit(1);",
820
+ "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
821
+ "const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
822
+ "",
823
+ 'if (cmd === "schema") {',
824
+ " const features = composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH });",
825
+ " process.exit(await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features }));",
826
+ "}",
827
+ "",
828
+ 'if (cmd === "consumer") {',
829
+ " process.exit(await runConsumerCli(rest, out));",
822
830
  "}",
823
831
  "",
824
- "const features = composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH });",
825
832
  "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
826
- "const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
827
- "process.exit(await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features }));",
833
+ 'console.error("\\n Unknown: kumiko " + (cmd ?? "") + " only \'kumiko schema <sub>\' or \'kumiko consumer <sub>\' in the standalone bundle.\\n");',
834
+ "process.exit(1);",
828
835
  "",
829
836
  ].join("\n");
830
837
  }