@cosmicdrift/kumiko-bundled-features 0.199.2 → 0.200.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-bundled-features",
3
- "version": "0.199.2",
3
+ "version": "0.200.1",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -25,6 +25,7 @@
25
25
  "./admin-shell/web": "./src/admin-shell/web/index.ts",
26
26
  "./compliance-profiles": "./src/compliance-profiles/index.ts",
27
27
  "./compliance-profiles/web": "./src/compliance-profiles/web/index.ts",
28
+ "./compliance-profiles-ops": "./src/compliance-profiles-ops/index.ts",
28
29
  "./config": "./src/config/index.ts",
29
30
  "./crypto-shredding": "./src/crypto-shredding/index.ts",
30
31
  "./config/web": "./src/config/web/index.ts",
@@ -125,12 +126,12 @@
125
126
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
126
127
  },
127
128
  "dependencies": {
128
- "@cosmicdrift/kumiko-dispatcher-live": "0.199.2",
129
- "@cosmicdrift/kumiko-framework": "0.199.2",
130
- "@cosmicdrift/kumiko-headless": "0.199.2",
131
- "@cosmicdrift/kumiko-renderer": "0.199.2",
132
- "@cosmicdrift/kumiko-renderer-web": "0.199.2",
133
- "@cosmicdrift/kumiko-types": "0.199.2",
129
+ "@cosmicdrift/kumiko-dispatcher-live": "0.200.1",
130
+ "@cosmicdrift/kumiko-framework": "0.200.1",
131
+ "@cosmicdrift/kumiko-headless": "0.200.1",
132
+ "@cosmicdrift/kumiko-renderer": "0.200.1",
133
+ "@cosmicdrift/kumiko-renderer-web": "0.200.1",
134
+ "@cosmicdrift/kumiko-types": "0.200.1",
134
135
  "@mollie/api-client": "^4.5.0",
135
136
  "@node-rs/argon2": "^2.0.2",
136
137
  "@types/mailparser": "^3.4.6",
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
4
+ import { createComplianceProfilesFeature } from "../../compliance-profiles";
5
+ import { createConfigFeature } from "../../config/feature";
6
+ import { createTenantFeature } from "../../tenant/feature";
7
+ import { complianceProfilesOpsFeature } from "../index";
8
+
9
+ describe("compliance-profiles-ops (#2089)", () => {
10
+ test("declares systemScope and requires compliance-profiles + tenant", () => {
11
+ expect(complianceProfilesOpsFeature.systemScope).toBe(true);
12
+ expect(complianceProfilesOpsFeature.requires).toContain("compliance-profiles");
13
+ expect(complianceProfilesOpsFeature.requires).toContain("tenant");
14
+ });
15
+
16
+ test("tenants-missing-profile is SystemAdmin-only", () => {
17
+ expect(
18
+ rolesOf(complianceProfilesOpsFeature.queryHandlers["tenants-missing-profile"]?.access),
19
+ ).toEqual(["SystemAdmin"]);
20
+ });
21
+
22
+ test("boot-validates alongside compliance-profiles + tenant + config", () => {
23
+ expect(() =>
24
+ validateBoot([
25
+ createConfigFeature(),
26
+ createTenantFeature(),
27
+ createComplianceProfilesFeature(),
28
+ complianceProfilesOpsFeature,
29
+ ]),
30
+ ).not.toThrow();
31
+ });
32
+
33
+ test("boot fails without tenant mounted (hard requires)", () => {
34
+ expect(() =>
35
+ validateBoot([
36
+ createConfigFeature(),
37
+ createComplianceProfilesFeature(),
38
+ complianceProfilesOpsFeature,
39
+ ]),
40
+ ).toThrow();
41
+ });
42
+ });
@@ -0,0 +1,78 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
3
+ import {
4
+ createTestUser,
5
+ setupTestStack,
6
+ type TestStack,
7
+ testTenantId,
8
+ unsafeCreateEntityTable,
9
+ } from "@cosmicdrift/kumiko-framework/stack";
10
+ import {
11
+ createComplianceProfilesFeature,
12
+ tenantComplianceProfileEntity,
13
+ } from "../../compliance-profiles";
14
+ import { seedComplianceProfile } from "../../compliance-profiles/seeding";
15
+ import { createConfigFeature } from "../../config/feature";
16
+ import { TenantHandlers } from "../../tenant/constants";
17
+ import { createTenantFeature } from "../../tenant/feature";
18
+ import { tenantEntity } from "../../tenant/schema/tenant";
19
+ import { complianceProfilesOpsFeature } from "../index";
20
+
21
+ const TENANTS_MISSING_PROFILE = "compliance-profiles-ops:query:tenants-missing-profile";
22
+
23
+ let stack: TestStack;
24
+
25
+ const opsAdmin = createTestUser({ id: 900, tenantId: testTenantId(900), roles: ["SystemAdmin"] });
26
+ const tenantWithProfile = testTenantId(901);
27
+ const tenantWithoutProfile = testTenantId(902);
28
+ const disabledTenantWithoutProfile = testTenantId(903);
29
+
30
+ beforeAll(async () => {
31
+ stack = await setupTestStack({
32
+ features: [
33
+ createConfigFeature(),
34
+ createTenantFeature(),
35
+ createComplianceProfilesFeature(),
36
+ complianceProfilesOpsFeature,
37
+ ],
38
+ });
39
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
40
+ await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
41
+ await createEventsTable(stack.db);
42
+
43
+ for (const [id, key] of [
44
+ [tenantWithProfile, "with-profile"],
45
+ [tenantWithoutProfile, "without-profile"],
46
+ [disabledTenantWithoutProfile, "disabled-without-profile"],
47
+ ] as const) {
48
+ await stack.http.writeOk(TenantHandlers.create, { id, key, name: key }, opsAdmin);
49
+ }
50
+ await stack.http.writeOk(TenantHandlers.disable, { id: disabledTenantWithoutProfile }, opsAdmin);
51
+ await seedComplianceProfile(stack.db, { tenantId: tenantWithProfile, profileKey: "eu-dsgvo" });
52
+ });
53
+
54
+ afterAll(async () => {
55
+ await stack.cleanup();
56
+ });
57
+
58
+ describe("tenants-missing-profile (#2089)", () => {
59
+ test("SystemAdmin sees only the enabled tenant without a profile", async () => {
60
+ const result = await stack.http.queryOk<{
61
+ tenants: readonly { id: string; name: string }[];
62
+ }>(TENANTS_MISSING_PROFILE, {}, opsAdmin);
63
+
64
+ const ids = result.tenants.map((t) => t.id);
65
+ expect(ids).toContain(tenantWithoutProfile);
66
+ expect(ids).not.toContain(tenantWithProfile);
67
+ expect(ids).not.toContain(disabledTenantWithoutProfile);
68
+ });
69
+
70
+ test("TenantAdmin gets 403", async () => {
71
+ const tenantAdmin = createTestUser({
72
+ id: 904,
73
+ tenantId: tenantWithoutProfile,
74
+ roles: ["TenantAdmin"],
75
+ });
76
+ expect((await stack.http.query(TENANTS_MISSING_PROFILE, {}, tenantAdmin)).status).toBe(403);
77
+ });
78
+ });
@@ -0,0 +1,54 @@
1
+ import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
2
+ import { defineQueryHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { z } from "zod";
5
+ import { tenantComplianceProfileTable } from "../../compliance-profiles";
6
+ import { tenantTable } from "../../tenant";
7
+
8
+ // SystemAdmin platform-wide counterpart to needs-profile (#2089).
9
+ //
10
+ // #2084 stopped needs-profile from nagging a TenantAdmin who can no longer
11
+ // reach a picker narrowed to access.systemAdmin — correct, but it left no
12
+ // one else able to notice: needs-profile stays TenantAdmin-only on purpose
13
+ // (widening its call-access was explicitly rejected in #2084), so a
14
+ // SystemAdmin who owns the platform-only picker has no way to see which
15
+ // tenants still run on minimal-no-region. This query fills that gap
16
+ // instead of touching needs-profile: every enabled tenant that has no row
17
+ // in tenantComplianceProfile at all, tenant-wide rather than for the
18
+ // caller's own tenant.
19
+ export const tenantsMissingProfileQuery = defineQueryHandler({
20
+ name: "tenants-missing-profile",
21
+ schema: z.object({}),
22
+ access: { roles: [ROLES.SystemAdmin] },
23
+ handler: async (_query, ctx): Promise<TenantsMissingProfileResponse> => {
24
+ if (!ctx.systemDb) {
25
+ throw new InternalError({
26
+ message:
27
+ "[compliance-profiles-ops] tenants-missing-profile requires ctx.systemDb — the " +
28
+ "compliance-profiles-ops feature must stay r.systemScope() (see feature.ts).",
29
+ });
30
+ }
31
+ const db = ctx.systemDb.acknowledgeCrossTenant(
32
+ "platform operator scans every tenant for a missing compliance-profile selection",
33
+ );
34
+
35
+ const tenants = await db.selectMany<{ id: TenantId; name: string }>(tenantTable, {
36
+ isEnabled: true,
37
+ });
38
+ const profileRows = await db.selectMany<{ tenantId: TenantId }>(
39
+ tenantComplianceProfileTable,
40
+ {},
41
+ );
42
+ const tenantIdsWithProfile = new Set(profileRows.map((row) => row.tenantId));
43
+
44
+ return {
45
+ tenants: tenants
46
+ .filter((tenant) => !tenantIdsWithProfile.has(tenant.id))
47
+ .map((tenant) => ({ id: tenant.id, name: tenant.name })),
48
+ };
49
+ },
50
+ });
51
+
52
+ interface TenantsMissingProfileResponse {
53
+ readonly tenants: readonly { readonly id: TenantId; readonly name: string }[];
54
+ }
@@ -0,0 +1,34 @@
1
+ // compliance-profiles-ops — platform-wide SystemAdmin visibility into
2
+ // tenants missing a compliance-profile selection (#2089).
3
+ //
4
+ // needs-profile (compliance-profiles) tells a TenantAdmin they must pick a
5
+ // profile, but stays TenantAdmin-only by design (#2084) — a SystemAdmin who
6
+ // owns a picker narrowed to access.systemAdmin has no query at all. Kept as
7
+ // a separate feature (mirrors folders-user-data/notes-history-user-data)
8
+ // rather than folded into compliance-profiles: this is the one genuinely
9
+ // cross-tenant capability in the picker's orbit, and it alone needs
10
+ // r.systemScope() + a hard r.requires("tenant") — apps that only want the
11
+ // per-tenant picker (the vast majority) stay unaffected.
12
+
13
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
14
+ import { tenantsMissingProfileQuery } from "./handlers/tenants-missing-profile.query";
15
+
16
+ export const complianceProfilesOpsFeature = defineFeature("compliance-profiles-ops", (r) => {
17
+ r.describe(
18
+ "Platform-wide SystemAdmin counterpart to `compliance-profiles`' `needs-profile` query (#2089): `tenants-missing-profile` lists every enabled tenant with no row in `tenantComplianceProfile` at all, tenant-wide instead of scoped to the caller's own tenant. Mount alongside `compliance-profiles` and `tenant` when an operator UI needs to see which tenants still silently run on `minimal-no-region`.",
19
+ );
20
+ r.uiHints({
21
+ displayLabel: "Compliance Profiles · Operator Visibility",
22
+ category: "compliance",
23
+ recommended: false,
24
+ });
25
+ r.systemScope();
26
+ r.requires("compliance-profiles");
27
+ r.requires("tenant");
28
+
29
+ const queries = {
30
+ tenantsMissingProfile: r.queryHandler(tenantsMissingProfileQuery),
31
+ };
32
+
33
+ return { queries };
34
+ });
@@ -1,6 +1,6 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection, DbRow } from "@cosmicdrift/kumiko-framework/db";
3
- import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+ import { createTenantDb, createUncheckedSystemDb } from "@cosmicdrift/kumiko-framework/db";
4
4
  import type { NotifyPriority, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
5
5
  import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
6
6
  import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
@@ -158,10 +158,18 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
158
158
  }
159
159
  const systemUser = createSystemUser(tenantId);
160
160
  const tenantDb = createTenantDb(db, tenantId, "system");
161
+ // Hand-built context, not routed through the dispatcher — tenantUserIdsQuery is
162
+ // typically an r.systemScope() handler, fail-closed on ctx.db, so this needs both.
161
163
  // @cast-boundary engine-payload — generic query-handler return for typed convention
162
164
  return (await handler.handler(
163
165
  { type: tenantUserIdsQuery, payload: { tenantId }, user: systemUser },
164
- { db: tenantDb, dbOutsideTransaction: tenantDb, registry, ...bridgeStub() },
166
+ {
167
+ db: tenantDb,
168
+ dbOutsideTransaction: tenantDb,
169
+ systemDb: createUncheckedSystemDb(tenantDb),
170
+ registry,
171
+ ...bridgeStub(),
172
+ },
165
173
  )) as readonly string[];
166
174
  }
167
175
 
@@ -16,6 +16,7 @@ import {
16
16
  type FeatureDefinition,
17
17
  SYSTEM_TENANT_ID,
18
18
  } from "@cosmicdrift/kumiko-framework/engine";
19
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
19
20
  import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
20
21
  import { createEventDispatcher, type EventConsumer } from "@cosmicdrift/kumiko-framework/pipeline";
21
22
  import {
@@ -63,7 +64,17 @@ function widgetFeature(): FeatureDefinition {
63
64
  r.writeHandler(
64
65
  "widget:create",
65
66
  z.object({ name: z.string().min(1).max(100), active: z.boolean().optional() }),
66
- async (event, ctx) => widgetCrud.create(event.payload, event.user, ctx.db),
67
+ async (event, ctx) => {
68
+ if (!ctx.systemDb) {
69
+ throw new InternalError({
70
+ message: "widget:create requires ctx.systemDb — is r.systemScope() still set?",
71
+ });
72
+ }
73
+ const db = ctx.systemDb.acknowledgeCrossTenant(
74
+ "widget catalog is system-wide in this test",
75
+ );
76
+ return widgetCrud.create(event.payload, event.user, db);
77
+ },
67
78
  { access: { roles: ["SystemAdmin"] } },
68
79
  );
69
80
  });
@@ -89,10 +100,13 @@ function widgetAuditFeature(): FeatureDefinition {
89
100
 
90
101
  r.hook("postSave", { allOf: "widget" }, async (result, ctx) => {
91
102
  if (result.kind !== "save" || !result.isNew) return;
92
- if (!ctx.db) return;
103
+ if (!ctx.systemDb) return;
93
104
  const name = result.changes!["name"] as string | undefined;
94
105
  if (!name) return;
95
- await seedRow(ctx.db, widgetAuditTable, {
106
+ const db = ctx.systemDb.acknowledgeCrossTenant(
107
+ "widget audit sink is system-wide in this test",
108
+ );
109
+ await seedRow(db, widgetAuditTable, {
96
110
  id: generateId(),
97
111
  widgetName: name,
98
112
  version: 1,
@@ -0,0 +1,13 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createStepDispatcherFeature } from "../feature";
4
+
5
+ describe("step-dispatcher (fw#2068)", () => {
6
+ test("does not declare systemScope — no handler reads ctx.db/ctx.systemDb", () => {
7
+ expect(createStepDispatcherFeature().systemScope).toBeFalsy();
8
+ });
9
+
10
+ test("boot-validates standalone", () => {
11
+ expect(() => validateBoot([createStepDispatcherFeature()])).not.toThrow();
12
+ });
13
+ });
@@ -37,7 +37,6 @@ export function createStepDispatcherFeature(): FeatureDefinition {
37
37
  category: "infrastructure",
38
38
  recommended: false,
39
39
  });
40
- r.systemScope();
41
40
 
42
41
  r.multiStreamProjection({
43
42
  name: "step-dispatcher",
@@ -1,6 +1,10 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { buildServer, type JwtHelper } from "@cosmicdrift/kumiko-framework/api";
3
- import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
+ import {
4
+ createTenantDb,
5
+ createUncheckedSystemDb,
6
+ type DbConnection,
7
+ } from "@cosmicdrift/kumiko-framework/db";
4
8
  import {
5
9
  createRegistry,
6
10
  defineFeature,
@@ -80,6 +84,9 @@ beforeAll(async () => {
80
84
  getActiveTenantIds: async () => {
81
85
  const handler = registry.getQueryHandler(TenantQueries.activeTenantIds);
82
86
  if (!handler) return [];
87
+ // Hand-built context, not routed through the dispatcher — active-tenant-ids
88
+ // is an r.systemScope() handler, fail-closed on ctx.db, needs ctx.systemDb too.
89
+ const systemModeDb = createTenantDb(db, "00000000-0000-4000-8000-000000000000", "system");
83
90
  const result = await handler.handler(
84
91
  {
85
92
  type: TenantQueries.activeTenantIds,
@@ -91,12 +98,9 @@ beforeAll(async () => {
91
98
  },
92
99
  },
93
100
  {
94
- db: createTenantDb(db, "00000000-0000-4000-8000-000000000000", "system"),
95
- dbOutsideTransaction: createTenantDb(
96
- db,
97
- "00000000-0000-4000-8000-000000000000",
98
- "system",
99
- ),
101
+ db: systemModeDb,
102
+ dbOutsideTransaction: systemModeDb,
103
+ systemDb: createUncheckedSystemDb(systemModeDb),
100
104
  registry,
101
105
  ...bridgeStub(),
102
106
  },
@@ -4,6 +4,7 @@ import {
4
4
  SYSTEM_ROLE,
5
5
  type TenantId,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
7
8
  import { z } from "zod";
8
9
  import { tenantTable } from "../schema/tenant";
9
10
 
@@ -12,9 +13,16 @@ export const activeTenantIdsQuery = defineQueryHandler({
12
13
  schema: z.object({}),
13
14
  access: { roles: [SYSTEM_ROLE, "SystemAdmin"] },
14
15
  handler: async (_query, ctx) => {
16
+ if (!ctx.systemDb) {
17
+ throw new InternalError({
18
+ message:
19
+ "tenant:query:activeTenantIds requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
20
+ });
21
+ }
22
+ const db = ctx.systemDb.acknowledgeCrossTenant("lists active tenant ids platform-wide");
15
23
  // tenants.id is a uuid string (TenantId), not a numeric surrogate.
16
24
  // Brand at the DB parse boundary — getActiveTenantIds consumers expect TenantId[].
17
- const rows = await selectMany<{ id: TenantId }>(ctx.db, tenantTable, { isEnabled: true });
25
+ const rows = await selectMany<{ id: TenantId }>(db, tenantTable, { isEnabled: true });
18
26
  return rows.map((r) => r.id);
19
27
  },
20
28
  });
@@ -1,7 +1,7 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
4
- import { ConflictError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { ConflictError, InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
5
5
  import { z } from "zod";
6
6
  import { TenantErrors } from "../constants";
7
7
  import { findForbiddenMembershipRole, reservedMembershipRoleError } from "../membership-roles";
@@ -20,7 +20,15 @@ export const addMemberWrite = defineWriteHandler({
20
20
  }),
21
21
  access: { roles: ["SystemAdmin"] },
22
22
  handler: async (event, ctx) => {
23
- const db = ctx.db;
23
+ if (!ctx.systemDb) {
24
+ throw new InternalError({
25
+ message:
26
+ "tenant:write:addMember requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
27
+ });
28
+ }
29
+ const db = ctx.systemDb.acknowledgeCrossTenant(
30
+ "SystemAdmin manages memberships across tenants",
31
+ );
24
32
  const forbidden = findForbiddenMembershipRole(event.payload.roles);
25
33
  if (forbidden !== undefined) return writeFailure(reservedMembershipRoleError(forbidden));
26
34
  const existing = await fetchOne(db, tenantMembershipsTable, {
@@ -1,21 +1,21 @@
1
- // Cancel-Handler für pending Invitations.
1
+ // Cancel handler for pending invitations.
2
2
  //
3
- // Admin sieht eine pending Invitation und entscheidet sie zurückzu-
4
- // nehmen (User soll doch nicht beitreten, falsche Email getippt etc.).
5
- // Effekt:
6
- // - DB-row.status → "cancelled"
7
- // - Token aus Redis gelöscht (gemerkt im invite-token-store)
3
+ // Admin sees a pending invitation and decides to withdraw it (user
4
+ // shouldn't join after all, wrong email typed, etc.).
5
+ // Effect:
6
+ // - DB row.status → "cancelled"
7
+ // - Token deleted from Redis (tracked in invite-token-store)
8
8
  //
9
- // Idempotent: cancellen einer schon-cancelled / accepted / expired
10
- // invitation = no-op + 200. Cancellen einer non-existent invitation
9
+ // Idempotent: cancelling an already-cancelled / accepted / expired
10
+ // invitation = no-op + 200. Cancelling a non-existent invitation
11
11
  // = invitation_not_found.
12
12
 
13
13
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
14
14
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
15
15
  import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
16
- import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
16
+ import { InternalError, NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
17
17
  import { z } from "zod";
18
- // kumiko-lint-ignore cross-feature-import cancel needs invite-token-store für Redis-cleanup
18
+ // kumiko-lint-ignore cross-feature-import cancel needs invite-token-store for Redis cleanup
19
19
  import {
20
20
  deleteInviteToken,
21
21
  getTokenForInvitation,
@@ -39,9 +39,20 @@ export const cancelInvitationWrite = defineWriteHandler({
39
39
  schema: CancelInvitationSchema,
40
40
  access: { roles: access.admin },
41
41
  handler: async (event, ctx) => {
42
- const invitation = await fetchOne(ctx.db.raw, tenantInvitationsTable, {
42
+ if (!ctx.systemDb) {
43
+ throw new InternalError({
44
+ message:
45
+ "tenant:write:cancel-invitation requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
46
+ });
47
+ }
48
+ const db = ctx.systemDb.assertTenantMatch(event.user.tenantId);
49
+ const invitation = await fetchOne(db, tenantInvitationsTable, {
43
50
  id: event.payload.invitationId,
51
+ tenantId: event.user.tenantId,
44
52
  });
53
+ // The tenantId check below is redundant with the where-clause above but
54
+ // kept as defense in depth — this is a security-cutover diff, not the
55
+ // place to also drop an existing check.
45
56
  if (!invitation || invitation["tenantId"] !== event.user.tenantId) {
46
57
  return writeFailure(
47
58
  new NotFoundError("tenantInvitation", event.payload.invitationId, {
@@ -50,7 +61,7 @@ export const cancelInvitationWrite = defineWriteHandler({
50
61
  );
51
62
  }
52
63
 
53
- // Idempotent: schon !pending → no-op success.
64
+ // Idempotent: already !pending → no-op success.
54
65
  if (invitation["status"] !== INVITATION_STATUS.pending) {
55
66
  return { isSuccess: true, data: { id: event.payload.invitationId, alreadyDone: true } };
56
67
  }
@@ -63,13 +74,13 @@ export const cancelInvitationWrite = defineWriteHandler({
63
74
  changes: { status: INVITATION_STATUS.cancelled },
64
75
  },
65
76
  event.user,
66
- ctx.db,
77
+ db,
67
78
  );
68
79
  if (!updateResult.isSuccess) return updateResult;
69
80
 
70
- // Token aus Redis löschen (falls noch da). Wenn Redis nicht
71
- // verfügbar oder Token schon expired: kein Problem, DB-row ist
72
- // die Single-Source für UI.
81
+ // Delete the token from Redis (if still there). If Redis is
82
+ // unavailable or the token already expired: not a problem, the DB
83
+ // row is the single source of truth for the UI.
73
84
  if (ctx.redis) {
74
85
  const token = await getTokenForInvitation(ctx.redis, event.payload.invitationId);
75
86
  if (token) {
@@ -1,14 +1,15 @@
1
1
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { z } from "zod";
4
5
  import { tenantEntity, tenantTable } from "../schema/tenant";
5
6
 
6
7
  const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
7
8
 
8
- // Optional `id`: SystemAdmin-only handler — legitimer Pfad für Seeds und
9
- // externe Provisionierung (SCIM, IdP-Sync, Migration aus bestehenden Systemen),
10
- // wo der Tenant mit einer vom Caller gewählten UUID angelegt werden muss.
11
- // Wenn nicht gesetzt, Postgres vergibt via gen_random_uuid() eine neue UUID.
9
+ // Optional `id`: SystemAdmin-only handler — legitimate path for seeds and
10
+ // external provisioning (SCIM, IdP sync, migration from existing systems),
11
+ // where the tenant must be created with a caller-chosen UUID. When unset,
12
+ // Postgres assigns a new UUID via gen_random_uuid().
12
13
  export const createWrite = defineWriteHandler({
13
14
  name: "create",
14
15
  schema: z.object({
@@ -16,10 +17,19 @@ export const createWrite = defineWriteHandler({
16
17
  key: z.string().min(1).max(50),
17
18
  name: z.string().min(1).max(200),
18
19
  }),
19
- // "system" + "SystemAdmin" — symmetrisch zu update-member-roles.
20
- // ops-tooling (seed-migrations + sample-recipes) nutzen System-User
21
- // (roles=["system"]) als Executor; "SystemAdmin" bleibt der echte
22
- // human-Operator-Pfad über die UI.
20
+ // "system" + "SystemAdmin" — symmetric to update-member-roles. Ops
21
+ // tooling (seed migrations + sample recipes) uses the system user
22
+ // (roles=["system"]) as the executor; "SystemAdmin" stays the real
23
+ // human-operator path via the UI.
23
24
  access: { roles: ["system", "SystemAdmin"] },
24
- handler: async (event, ctx) => crud.create(event.payload, event.user, ctx.db),
25
+ handler: async (event, ctx) => {
26
+ if (!ctx.systemDb) {
27
+ throw new InternalError({
28
+ message:
29
+ "tenant:write:create requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
30
+ });
31
+ }
32
+ const db = ctx.systemDb.acknowledgeCrossTenant("creating a tenant is inherently cross-tenant");
33
+ return crud.create(event.payload, event.user, db);
34
+ },
25
35
  });
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { z } from "zod";
4
5
  import { decryptStoredPii, mapWithConcurrency } from "../../shared";
5
6
  import { INVITATION_STATUS, tenantInvitationsTable } from "../invitation-table";
@@ -23,7 +24,14 @@ export const invitationsQuery = defineQueryHandler({
23
24
  schema: z.object({}),
24
25
  access: { roles: access.admin },
25
26
  handler: async (query, ctx) => {
26
- const rows = await selectMany<Record<string, unknown>>(ctx.db, tenantInvitationsTable, {
27
+ if (!ctx.systemDb) {
28
+ throw new InternalError({
29
+ message:
30
+ "tenant:query:invitations requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
31
+ });
32
+ }
33
+ const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
34
+ const rows = await selectMany<Record<string, unknown>>(db, tenantInvitationsTable, {
27
35
  tenantId: query.user.tenantId,
28
36
  status: INVITATION_STATUS.pending,
29
37
  });
@@ -1,5 +1,6 @@
1
1
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { z } from "zod";
4
5
  import { tenantEntity, tenantTable } from "../schema/tenant";
5
6
 
@@ -13,5 +14,14 @@ export const listQuery = defineQueryHandler({
13
14
  search: z.string().optional(),
14
15
  }),
15
16
  access: { roles: ["SystemAdmin"] },
16
- handler: async (query, ctx) => crud.list(query.payload, query.user, ctx.db), // @wrapper-known semantic-alias
17
+ handler: async (query, ctx) => {
18
+ if (!ctx.systemDb) {
19
+ throw new InternalError({
20
+ message:
21
+ "tenant:query:list requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
22
+ });
23
+ }
24
+ const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin lists tenants platform-wide");
25
+ return crud.list(query.payload, query.user, db); // @wrapper-known semantic-alias
26
+ },
17
27
  });
@@ -1,16 +1,24 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { z } from "zod";
4
5
  import { tenantTable } from "../schema/tenant";
5
6
 
6
- // Direct query — query-handlers haben keinen tenant-crud-Handle. Direct-select
7
- // ist trivial: WHERE id = tenantId (beides UUID). Kein CRUD-Detour nötig.
7
+ // Direct query — query handlers don't have a tenant-crud handle. A direct
8
+ // select is trivial: WHERE id = tenantId (both UUID). No CRUD detour needed.
8
9
  export const meQuery = defineQueryHandler({
9
10
  name: "me",
10
11
  schema: z.object({}),
11
12
  access: { openToAll: true },
12
13
  handler: async (query, ctx) => {
13
- const row = await fetchOne(ctx.db, tenantTable, { id: query.user.tenantId });
14
+ if (!ctx.systemDb) {
15
+ throw new InternalError({
16
+ message:
17
+ "tenant:query:me requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
18
+ });
19
+ }
20
+ const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
21
+ const row = await fetchOne(db, tenantTable, { id: query.user.tenantId });
14
22
  return row ?? null;
15
23
  },
16
24
  });
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
4
5
  import { z } from "zod";
5
6
  import { decryptStoredPii, mapWithConcurrency } from "../../shared";
@@ -20,13 +21,20 @@ export const membersQuery = defineQueryHandler({
20
21
  schema: z.object({}),
21
22
  access: { roles: access.admin },
22
23
  handler: async (query, ctx) => {
23
- const rows = await selectMany(ctx.db, tenantMembershipsTable, {
24
+ if (!ctx.systemDb) {
25
+ throw new InternalError({
26
+ message:
27
+ "tenant:query:members requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
28
+ });
29
+ }
30
+ const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
31
+ const rows = await selectMany(db, tenantMembershipsTable, {
24
32
  tenantId: query.user.tenantId,
25
33
  });
26
34
 
27
35
  const userIds = [...new Set(rows.map((row) => row["userId"]))];
28
36
  const users =
29
- userIds.length > 0 ? await selectMany<UserRow>(ctx.db, userTable, { id: userIds }) : [];
37
+ userIds.length > 0 ? await selectMany<UserRow>(db, userTable, { id: userIds }) : [];
30
38
  const userById = new Map(users.map((u) => [String(u.id), u]));
31
39
 
32
40
  const decrypted = await mapWithConcurrency(users, KMS_POOL_CONCURRENCY, async (user) => {
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineQueryHandler, SYSTEM_ROLE } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
4
5
  import { z } from "zod";
5
6
  import { tenantMembershipsTable } from "../membership-table";
@@ -12,14 +13,24 @@ export const membershipsQuery = defineQueryHandler({
12
13
  // directly by tenant admins managing memberships in the admin UI.
13
14
  access: { roles: [SYSTEM_ROLE, "SystemAdmin"] },
14
15
  handler: async (query, ctx) => {
15
- const rows = await selectMany(ctx.db, tenantMembershipsTable, { userId: query.payload.userId });
16
+ if (!ctx.systemDb) {
17
+ throw new InternalError({
18
+ message:
19
+ "tenant:query:memberships requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
20
+ });
21
+ }
22
+ const db = ctx.systemDb.acknowledgeCrossTenant(
23
+ "resolves memberships for an arbitrary userId across all tenants",
24
+ );
25
+ const rows = await selectMany(db, tenantMembershipsTable, { userId: query.payload.userId });
16
26
  if (rows.length === 0) return [];
17
27
 
18
- // tenantName/tenantKey machen Memberships im UI unterscheidbar (sonst nur
19
- // das UUID-PräfixSeed-Tenants mit 00000000-…-Präfix wären ununterscheidbar).
20
- // Ein einzelner IN-Batch über alle tenantIds statt fetchOne pro Membership (#324).
28
+ // tenantName/tenantKey make memberships distinguishable in the UI
29
+ // (otherwise just the UUID prefix seed tenants with a 00000000-…
30
+ // prefix would be indistinguishable). A single IN-batch over all
31
+ // tenantIds instead of fetchOne per membership (#324).
21
32
  type TenantRow = { id: unknown; name?: unknown; key?: unknown; isEnabled?: unknown };
22
- const tenants = await selectMany<TenantRow>(ctx.db, tenantTable, {
33
+ const tenants = await selectMany<TenantRow>(db, tenantTable, {
23
34
  id: rows.map((row) => row["tenantId"]),
24
35
  });
25
36
  const tenantById = new Map<unknown, TenantRow>(tenants.map((t) => [t.id, t]));
@@ -27,11 +38,11 @@ export const membershipsQuery = defineQueryHandler({
27
38
  return rows
28
39
  .map((row) => {
29
40
  const tenant = tenantById.get(row["tenantId"]);
30
- // Disabled Tenants (tenant:write:disable) zählen nicht als Membership:
31
- // Login wählt sie nicht, /auth/tenants listet sie nicht, switch-tenant
32
- // antwortet not_a_member. Nur das explizite false filtert — eine
33
- // fehlende tenant-Row (Projektions-Drift) soll keinen Login-Lockout
34
- // aller Member auslösen.
41
+ // Disabled tenants (tenant:write:disable) don't count as a
42
+ // membership: login doesn't pick them, /auth/tenants doesn't list
43
+ // them, switch-tenant answers not_a_member. Only the explicit
44
+ // false filters — a missing tenant row (projection drift) should
45
+ // not lock out every member's login.
35
46
  if (tenant !== undefined && tenant.isEnabled === false) return null;
36
47
  return {
37
48
  ...row,
@@ -5,7 +5,7 @@ import {
5
5
  defineWriteHandler,
6
6
  withResponseData,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
- import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
8
+ import { InternalError, NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
9
  import { z } from "zod";
10
10
  import { tenantMembershipEntity, tenantMembershipsTable } from "../membership-table";
11
11
 
@@ -23,7 +23,15 @@ export const removeMemberWrite = defineWriteHandler({
23
23
  schema: z.object({ userId: z.string(), tenantId: z.string() }),
24
24
  access: { roles: ["SystemAdmin"] },
25
25
  handler: async (event, ctx) => {
26
- const db = ctx.db;
26
+ if (!ctx.systemDb) {
27
+ throw new InternalError({
28
+ message:
29
+ "tenant:write:removeMember requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
30
+ });
31
+ }
32
+ const db = ctx.systemDb.acknowledgeCrossTenant(
33
+ "SystemAdmin manages memberships across tenants",
34
+ );
27
35
  const existing = await fetchOne(db, tenantMembershipsTable, {
28
36
  userId: event.payload.userId,
29
37
  tenantId: event.payload.tenantId,
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineQueryHandler, SYSTEM_ROLE } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { z } from "zod";
4
5
  import { tenantMembershipsTable } from "../membership-table";
5
6
 
@@ -14,17 +15,26 @@ export const resolveUserIdsQuery = defineQueryHandler({
14
15
  }),
15
16
  access: { roles: [SYSTEM_ROLE] },
16
17
  handler: async (query, ctx) => {
18
+ if (!ctx.systemDb) {
19
+ throw new InternalError({
20
+ message:
21
+ "tenant:query:resolveUserIds requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
22
+ });
23
+ }
24
+ const db = ctx.systemDb.acknowledgeCrossTenant(
25
+ "cross-feature lookup by arbitrary tenantId/userId",
26
+ );
17
27
  const { tenantId, userId } = query.payload;
18
28
 
19
29
  if (tenantId !== undefined) {
20
- const rows = await selectMany<{ userId: number }>(ctx.db, tenantMembershipsTable, {
30
+ const rows = await selectMany<{ userId: number }>(db, tenantMembershipsTable, {
21
31
  tenantId,
22
32
  });
23
33
  return rows.map((r) => r.userId);
24
34
  }
25
35
 
26
36
  if (userId !== undefined) {
27
- const rows = await selectMany(ctx.db, tenantMembershipsTable, { userId });
37
+ const rows = await selectMany(db, tenantMembershipsTable, { userId });
28
38
  return rows.length > 0 ? [userId] : [];
29
39
  }
30
40
 
@@ -1,5 +1,6 @@
1
1
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { z } from "zod";
4
5
  import { tenantEntity, tenantTable } from "../schema/tenant";
5
6
 
@@ -8,14 +9,24 @@ const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "
8
9
  // Admin flip: last-writer-wins is fine. SystemAdmin is the only caller and
9
10
  // there's no meaningful concurrent-edit race on this single boolean.
10
11
  function createToggleTenantHandler(enable: boolean) {
12
+ const verbName = enable ? "enable" : "disable";
11
13
  return defineWriteHandler({
12
- name: enable ? "enable" : "disable",
14
+ name: verbName,
13
15
  schema: z.object({ id: z.uuid() }),
14
16
  access: { roles: ["SystemAdmin"] },
15
- handler: async (event, ctx) =>
16
- crud.update({ id: event.payload.id, changes: { isEnabled: enable } }, event.user, ctx.db, {
17
+ handler: async (event, ctx) => {
18
+ if (!ctx.systemDb) {
19
+ throw new InternalError({
20
+ message: `tenant:write:${verbName} requires ctx.systemDb — is r.systemScope() still set on the tenant feature?`,
21
+ });
22
+ }
23
+ const db = ctx.systemDb.acknowledgeCrossTenant(
24
+ "SystemAdmin enables/disables tenants platform-wide",
25
+ );
26
+ return crud.update({ id: event.payload.id, changes: { isEnabled: enable } }, event.user, db, {
17
27
  skipOptimisticLock: true,
18
- }), // @wrapper-known semantic-alias
28
+ }); // @wrapper-known semantic-alias
29
+ },
19
30
  });
20
31
  }
21
32
 
@@ -5,7 +5,7 @@ import {
5
5
  defineWriteHandler,
6
6
  withResponseData,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
- import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
8
+ import { InternalError, NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
9
  import { z } from "zod";
10
10
  import { findForbiddenMembershipRole, reservedMembershipRoleError } from "../membership-roles";
11
11
  import { tenantMembershipEntity, tenantMembershipsTable } from "../membership-table";
@@ -26,13 +26,21 @@ export const updateMemberRolesWrite = defineWriteHandler({
26
26
  tenantId: z.string(),
27
27
  roles: z.array(z.string()).min(1),
28
28
  }),
29
- // "system" + "SystemAdmin" — symmetrisch zu tenant:write:create. System-
30
- // User (createSystemUser, roles=["system"]) braucht den Access für seed-
31
- // migrations + andere ops-tooling-Pfade. SystemAdmin ist der echte
32
- // human-Operator-Pfad über die UI.
29
+ // "system" + "SystemAdmin" — symmetric to tenant:write:create. The
30
+ // system user (createSystemUser, roles=["system"]) needs access for
31
+ // seed migrations and other ops-tooling paths. SystemAdmin is the real
32
+ // human-operator path via the UI.
33
33
  access: { roles: ["system", "SystemAdmin"] },
34
34
  handler: async (event, ctx) => {
35
- const db = ctx.db;
35
+ if (!ctx.systemDb) {
36
+ throw new InternalError({
37
+ message:
38
+ "tenant:write:updateMemberRoles requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
39
+ });
40
+ }
41
+ const db = ctx.systemDb.acknowledgeCrossTenant(
42
+ "SystemAdmin manages memberships across tenants",
43
+ );
36
44
  const forbidden = findForbiddenMembershipRole(event.payload.roles);
37
45
  if (forbidden !== undefined) return writeFailure(reservedMembershipRoleError(forbidden));
38
46
  const existing = await fetchOne(db, tenantMembershipsTable, {
@@ -340,28 +340,28 @@ export function createTierEngineFeature<
340
340
  const newTenantId = data.id as TenantId; // @cast-boundary engine-payload
341
341
  const aggregateId = tierAssignmentAggregateId(newTenantId);
342
342
 
343
- // skip: defensive inTransaction phase hat ctx.db immer gesetzt,
344
- // aber AppContext type macht's optional. Throw wäre overreach
345
- // (lifecycle blocking), silent-skip ist defensive-soft.
346
- if (!ctx.db) return;
347
-
348
- // ctx.db ist im inTransaction-phase eine TenantDb (tenant-scoped
349
- // proxy auf die echte TX). Für event-store-Pfade brauchen wir
350
- // die rohe DbConnection TenantDb exposes nur select/insert/
351
- // update/delete, NICHT execute (event-store-append.ts:102 ruft
352
- // db.execute(sql`SELECT pg_notify(...)`) TypeError sonst).
353
- // Pattern matched signup-confirm.write.ts:107 (.raw), nicht
354
- // `as DbConnection` — das ist Type-Lie der erst beim ersten
355
- // .execute()-Call crashed.
356
- //
357
- // AppContext.db ist union (DbConnection | TenantDb). Im
358
- // inTransaction-phase garantiert TenantDb — der dispatcher
359
- // wrapped vorher (siehe pipeline/dispatcher.ts createTenantDb-
360
- // Aufruf). TypeGuard via `"raw" in ...` ist robuster als
361
- // `as TenantDb` gegen future refactor.
362
- // skip: defensive — sollte im inTransaction nie greifen.
363
- if (!("raw" in ctx.db)) return;
364
- const rawDb = ctx.db.raw as DbConnection; // @cast-boundary db-runner
343
+ // tenant is an r.systemScope() feature, so this cross-feature hook gets a
344
+ // fail-closed ctx.db reach for ctx.systemDb instead (see entity-handlers.ts).
345
+ const db = ctx.systemDb
346
+ ? ctx.systemDb.acknowledgeCrossTenant(
347
+ `tier-engine auto-default-tier hook on r.systemScope() tenant write (${newTenantId})`,
348
+ )
349
+ : ctx.db;
350
+ // skip: defensiveinTransaction phase always sets db, but AppContext's
351
+ // type makes it optional. Throwing would be overreach (lifecycle
352
+ // blocking), silent-skip is defensive-soft.
353
+ if (!db) return;
354
+
355
+ // db is a TenantDb in the inTransaction phase (tenant-scoped proxy over
356
+ // the real tx). Event-store paths need the raw DbConnection — TenantDb
357
+ // only exposes select/insert/update/delete, not execute
358
+ // (event-store-append.ts:102 calls db.execute(sql`SELECT pg_notify(...)`)
359
+ // TypeError otherwise). Pattern matches signup-confirm.write.ts:107
360
+ // (.raw), not `as DbConnection` that's a type-lie that only crashes on
361
+ // the first .execute() call.
362
+ // skip: defensive — should never trip in the inTransaction phase.
363
+ if (!("raw" in db)) return;
364
+ const rawDb = db.raw as DbConnection; // @cast-boundary db-runner
365
365
 
366
366
  // Idempotency: stream-existence-check vor create. Pattern aus
367
367
  // seedTenant.ts. Bei re-replay (rebuild) nicht versionsbumpen.