@cosmicdrift/kumiko-samples 0.354.0 → 0.355.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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +2 -0
  4. package/packages/bundled-features/src/agent-tools/types.ts +2 -0
  5. package/packages/bundled-features/src/audit/escape-hatch-audit-sink.ts +2 -0
  6. package/packages/bundled-features/src/audit/handlers/resolve-audit-tenant.ts +8 -7
  7. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +7 -12
  8. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +13 -17
  9. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +12 -8
  10. package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +1 -12
  11. package/packages/bundled-features/src/delivery/delivery-service.ts +1 -1
  12. package/packages/bundled-features/src/managed-pages/handlers/by-tenant-published.query.ts +1 -10
  13. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -12
  14. package/packages/bundled-features/src/template-resolver/handlers/by-slug.query.ts +1 -10
  15. package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -10
  16. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +0 -7
  17. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +0 -7
  18. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +0 -8
  19. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +0 -8
  20. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +0 -7
  21. package/packages/bundled-features/src/template-resolver/seeding.ts +1 -2
  22. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -0
  23. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -0
  24. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -0
  25. package/packages/bundled-features/src/user-data-rights/feature.ts +2 -0
  26. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +5 -0
  27. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +11 -1
  28. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-samples",
3
- "version": "0.354.0",
3
+ "version": "0.355.0",
4
4
  "description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.354.0",
3
+ "version": "0.355.0",
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>",
@@ -251,6 +251,7 @@ function buildHandlerEntry(
251
251
  }
252
252
 
253
253
  const entity = getHandlerEntity(qn);
254
+ const excludedFields = "excludedFields" in def ? def.excludedFields : undefined;
254
255
  return {
255
256
  qn,
256
257
  kind,
@@ -258,6 +259,7 @@ function buildHandlerEntry(
258
259
  risk: exposure.risk,
259
260
  inputSchema,
260
261
  ...(entity !== undefined && { entity }),
262
+ ...(excludedFields !== undefined && { excludedFields }),
261
263
  };
262
264
  }
263
265
 
@@ -152,6 +152,8 @@ export type AgentManifestHandler = {
152
152
  readonly risk: AgentRisk;
153
153
  readonly inputSchema: Readonly<Record<string, unknown>>;
154
154
  readonly entity?: string;
155
+ /** Fields this generic create/update handler never writes; sending one fails validation. */
156
+ readonly excludedFields?: readonly string[];
155
157
  };
156
158
 
157
159
  export type AgentManifestScreen = {
@@ -23,6 +23,7 @@ export const escapeHatchUsedSchema = z.object({
23
23
  kind: z.enum(escapeHatchKinds),
24
24
  reason: z.string(),
25
25
  actor: z.string(),
26
+ caller: z.string().optional(),
26
27
  targetUserId: z.string().optional(),
27
28
  targetTenantId: z.string().optional(),
28
29
  });
@@ -36,6 +37,7 @@ export function createEscapeHatchAuditSink(opts: {
36
37
  kind: event.kind,
37
38
  reason: event.reason,
38
39
  actor: event.actor,
40
+ ...(event.caller !== undefined && { caller: event.caller }),
39
41
  ...(event.target && { targetUserId: event.target.id, targetTenantId: event.target.tenantId }),
40
42
  });
41
43
  // Appends on the unbound pool, never the handler tx — an audit entry must
@@ -1,4 +1,5 @@
1
- import { crossTenantOverrideDenied, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { mayOverrideTenant, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { AccessDeniedError, FrameworkReasons } from "@cosmicdrift/kumiko-framework/errors";
2
3
  import { APP_INSTANCE_STREAM_TYPE } from "@cosmicdrift/kumiko-framework/event-store";
3
4
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-types/identifiers";
4
5
  import { type AuditScope, AuditScopes } from "../constants.js";
@@ -12,11 +13,11 @@ export function resolveAuditScopeFilter(
12
13
  scope: AuditScope | undefined,
13
14
  ): AuditScopeFilter {
14
15
  if (scope !== AuditScopes.system) return { tenantId: user.tenantId };
15
- const denied = crossTenantOverrideDenied(
16
- user,
17
- SYSTEM_TENANT_ID,
18
- "audit.errors.systemScopeRequiresSystemAdmin",
19
- );
20
- if (denied) throw denied;
16
+ if (!mayOverrideTenant(user)) {
17
+ throw new AccessDeniedError({
18
+ i18nKey: "audit.errors.systemScopeRequiresSystemAdmin",
19
+ details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
20
+ });
21
+ }
21
22
  return { tenantId: SYSTEM_TENANT_ID, aggregateType: APP_INSTANCE_STREAM_TYPE };
22
23
  }
@@ -1,5 +1,4 @@
1
1
  import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
- import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
4
3
  import { SubscriptionFoundationHandlers, SubscriptionStatuses } from "../constants.js";
5
4
  import {
@@ -75,17 +74,13 @@ export function registerConsumerProtection(
75
74
  // user), never from payload data.
76
75
  const tenantId = ctx.systemUser.tenantId;
77
76
  const currentPeriodEndIso = payload["currentPeriodEndIso"];
78
- const result = await ctx.writeAs(
79
- createSystemUser(tenantId),
80
- SubscriptionFoundationHandlers.issueContractConfirmation,
81
- {
82
- consentId,
83
- sourceAggregateId: isSubscriptionEvent
84
- ? subscriptionAggregateId(tenantId)
85
- : paymentAggregateId(tenantId),
86
- ...(typeof currentPeriodEndIso === "string" && { currentPeriodEndIso }),
87
- },
88
- );
77
+ const result = await ctx.write(SubscriptionFoundationHandlers.issueContractConfirmation, {
78
+ consentId,
79
+ sourceAggregateId: isSubscriptionEvent
80
+ ? subscriptionAggregateId(tenantId)
81
+ : paymentAggregateId(tenantId),
82
+ ...(typeof currentPeriodEndIso === "string" && { currentPeriodEndIso }),
83
+ });
89
84
  if (!result.isSuccess) {
90
85
  throw new Error(
91
86
  `billing-foundation:${trigger.jobName}: issue-contract-confirmation failed: ${JSON.stringify(result.error)}`,
@@ -1,4 +1,4 @@
1
- import { createSystemUser, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
1
+ import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { SubscriptionCancelTimings, SubscriptionFoundationHandlers } from "../constants.js";
3
3
  import {
4
4
  CONTRACT_TERMINATION_DECLARED_EVENT_QN,
@@ -59,22 +59,18 @@ export function registerContractTermination(
59
59
  const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
60
60
  // skip: trigger event carries no usable declaration
61
61
  if (!declared.success) return;
62
- const result = await ctx.writeAs(
63
- createSystemUser(ctx.systemUser.tenantId),
64
- SubscriptionFoundationHandlers.recordContractTermination,
65
- {
66
- requestId: declared.data.requestId,
67
- declarationType: declared.data.declarationType,
68
- terminationKind: declared.data.terminationKind,
69
- channel: "public",
70
- when:
71
- declared.data.declarationType === "withdrawal"
72
- ? "none"
73
- : SubscriptionCancelTimings.periodEnd,
74
- receivedAtIso: declared.data.receivedAtIso,
75
- locale: declared.data.locale,
76
- },
77
- );
62
+ const result = await ctx.write(SubscriptionFoundationHandlers.recordContractTermination, {
63
+ requestId: declared.data.requestId,
64
+ declarationType: declared.data.declarationType,
65
+ terminationKind: declared.data.terminationKind,
66
+ channel: "public",
67
+ when:
68
+ declared.data.declarationType === "withdrawal"
69
+ ? "none"
70
+ : SubscriptionCancelTimings.periodEnd,
71
+ receivedAtIso: declared.data.receivedAtIso,
72
+ locale: declared.data.locale,
73
+ });
78
74
  if (!result.isSuccess) {
79
75
  throw new Error(
80
76
  `billing-foundation:cancel-on-public-termination-declared: record-contract-termination failed: ${JSON.stringify(result.error)}`,
@@ -1,12 +1,16 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
3
3
  import {
4
- crossTenantOverrideDenied,
5
4
  defineQueryHandler,
5
+ mayOverrideTenant,
6
6
  type QueryHandlerDefinition,
7
7
  type TenantId,
8
8
  } from "@cosmicdrift/kumiko-framework/engine";
9
- import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
9
+ import {
10
+ AccessDeniedError,
11
+ FrameworkReasons,
12
+ InternalError,
13
+ } from "@cosmicdrift/kumiko-framework/errors";
10
14
  import * as z from "zod";
11
15
  import { tierAssignmentEntity } from "../../tier-engine/index.js";
12
16
  import type { CapSpec, CapUsageWithMeta } from "../types.js";
@@ -39,12 +43,12 @@ export function createCapsUsageQuery(
39
43
  });
40
44
  }
41
45
  const override = query.payload.tenantId;
42
- const overrideDenied = crossTenantOverrideDenied(
43
- query.user,
44
- override,
45
- "cap-overview.errors.tenantOverrideRequiresSystemAdmin",
46
- );
47
- if (overrideDenied) throw overrideDenied;
46
+ if (override !== undefined && !mayOverrideTenant(query.user)) {
47
+ throw new AccessDeniedError({
48
+ i18nKey: "cap-overview.errors.tenantOverrideRequiresSystemAdmin",
49
+ details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
50
+ });
51
+ }
48
52
 
49
53
  const targetTenantId = override ?? query.user.tenantId;
50
54
  // Both branches return the SAME unfiltered system-mode db —
@@ -4,12 +4,7 @@ import {
4
4
  SELECTABLE_PROFILE_KEYS,
5
5
  } from "@cosmicdrift/kumiko-framework/compliance";
6
6
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
7
- import {
8
- access,
9
- crossTenantOverrideDenied,
10
- defineWriteHandler,
11
- type TenantId,
12
- } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { access, defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
13
8
  import {
14
9
  UnprocessableError,
15
10
  validationErrorFromZod,
@@ -63,12 +58,6 @@ export const setProfileWrite = defineWriteHandler({
63
58
  "Sets or replaces a tenant's compliance profile key plus an optional JSON override, for onboarding or a later region change; a system admin may target a different tenant through tenantIdOverride.",
64
59
  handler: async (event, ctx) => {
65
60
  const tenantOverride = event.payload.tenantIdOverride;
66
- const overrideDenied = crossTenantOverrideDenied(
67
- event.user,
68
- tenantOverride,
69
- "complianceProfiles.errors.tenantOverrideRequiresSystemAdmin",
70
- );
71
- if (overrideDenied) return writeFailure(overrideDenied);
72
61
  const tenantId = (tenantOverride ?? event.user.tenantId) as TenantId; // @cast-boundary engine-payload
73
62
  const executorUser = tenantOverride !== undefined ? { ...event.user, tenantId } : event.user;
74
63
 
@@ -190,7 +190,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
190
190
  tenantId,
191
191
  actor: systemUser.id,
192
192
  sink: escapeHatchAuditSink,
193
- log,
193
+ log: logError,
194
194
  });
195
195
  // ctx.db/dbOutsideTransaction are tenant-filtered even for a systemScope handler:
196
196
  // the dispatcher closes them off entirely there, so "system" mode would hand this
@@ -1,9 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
3
- import {
4
- crossTenantOverrideDenied,
5
- defineQueryHandler,
6
- } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
7
4
  import * as z from "zod";
8
5
  import { type PageRow, pagesTable } from "../table.js";
9
6
 
@@ -33,12 +30,6 @@ export const byTenantPublishedQuery = defineQueryHandler({
33
30
  "Lists every published page of a tenant with slug, language, title and last-change time but no body; use it to enumerate the public pages for sitemap.xml or llms.txt, rather than by-slug which fetches one page's content.",
34
31
  handler: async (query, ctx) => {
35
32
  const override = query.payload.tenantIdOverride;
36
- const overrideDenied = crossTenantOverrideDenied(
37
- query.user,
38
- override,
39
- "managedPages.errors.tenantOverrideRequiresSystemAdmin",
40
- );
41
- if (overrideDenied) throw overrideDenied;
42
33
  const tenantId = override ?? query.user.tenantId;
43
34
  const rows = castTenantRows<PageRow>(
44
35
  await selectMany(ctx.db, pagesTable, { tenantId: tenantId, published: true }),
@@ -1,11 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
3
- import {
4
- crossTenantOverrideDenied,
5
- defineWriteHandler,
6
- type TenantId,
7
- } from "@cosmicdrift/kumiko-framework/engine";
8
- import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
9
4
  import * as z from "zod";
10
5
  import { type PageRow, pageEntity, pagesTable } from "../table.js";
11
6
 
@@ -55,12 +50,6 @@ export const setWrite = defineWriteHandler({
55
50
  handler: async (event, ctx) => {
56
51
  const db = ctx.db;
57
52
  const override = event.payload.tenantIdOverride;
58
- const overrideDenied = crossTenantOverrideDenied(
59
- event.user,
60
- override,
61
- "managedPages.errors.tenantOverrideRequiresSystemAdmin",
62
- );
63
- if (overrideDenied) return writeFailure(overrideDenied);
64
53
  const tenantId = override ?? event.user.tenantId;
65
54
  // override: point the executor context at the target tenant, else getStreamVersion runs against user.tenantId → version_conflict.
66
55
  const executorUser =
@@ -1,8 +1,5 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import {
3
- crossTenantOverrideDenied,
4
- defineQueryHandler,
5
- } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
6
3
  import * as z from "zod";
7
4
  import { TEXT_BLOCK_KIND } from "../constants.js";
8
5
  import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
@@ -33,12 +30,6 @@ export const bySlugQuery = defineQueryHandler({
33
30
  "Reads one text-block of a tenant by slug and locale together with its body and format; it is pinned to the text-block kind so mail templates and AI prompts in the same table stay unreachable through this anonymous-capable path.",
34
31
  handler: async (query, ctx) => {
35
32
  const override = query.payload.tenantIdOverride;
36
- const overrideDenied = crossTenantOverrideDenied(
37
- query.user,
38
- override,
39
- "templateResolver.errors.tenantOverrideRequiresSystemAdmin",
40
- );
41
- if (overrideDenied) throw overrideDenied;
42
33
  const tenantId = override ?? query.user.tenantId;
43
34
  const row = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
44
35
  tenantId,
@@ -1,9 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
3
- import {
4
- crossTenantOverrideDenied,
5
- defineQueryHandler,
6
- } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
7
4
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
8
5
  import * as z from "zod";
9
6
  import { TEXT_BLOCK_KIND } from "../constants.js";
@@ -39,12 +36,6 @@ export const byTenantQuery = defineQueryHandler({
39
36
  "Lists every text-block of a tenant with slug, locale, title and body so a public content tree can be rendered in one call; use it for the whole sidebar, and by-slug when only one block is needed.",
40
37
  handler: async (query, ctx) => {
41
38
  const override = query.payload.tenantIdOverride;
42
- const overrideDenied = crossTenantOverrideDenied(
43
- query.user,
44
- override,
45
- "templateResolver.errors.tenantOverrideRequiresSystemAdmin",
46
- );
47
- if (overrideDenied) throw overrideDenied;
48
39
  const tenantId = override ?? query.user.tenantId;
49
40
  const rows = castTenantRows<TemplateResourceRow>(
50
41
  await selectMany(ctx.db, templateResourcesTable, { tenantId, kind: TEXT_BLOCK_KIND }),
@@ -1,7 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import {
3
3
  type ContentCollectionDefinition,
4
- crossTenantOverrideDenied,
5
4
  defineQueryHandler,
6
5
  } from "@cosmicdrift/kumiko-framework/engine";
7
6
  import * as z from "zod";
@@ -31,12 +30,6 @@ export function makeCollectionItemQuery(collection: ContentCollectionDefinition)
31
30
  : `Reads one tenant-wide "${collection.id}" entry by slug and locale, returning null when it does not exist; use it to open a single entry in the editor rather than to list the collection.`,
32
31
  handler: async (query, ctx) => {
33
32
  const override = query.payload.tenantIdOverride;
34
- const overrideDenied = crossTenantOverrideDenied(
35
- query.user,
36
- override,
37
- "templateResolver.errors.tenantOverrideRequiresSystemAdmin",
38
- );
39
- if (overrideDenied) throw overrideDenied;
40
33
  const tenantId = override ?? query.user.tenantId;
41
34
  const row = await fetchOne<CollectionEntryRow>(ctx.db, store.table, {
42
35
  tenantId,
@@ -2,7 +2,6 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
3
3
  import {
4
4
  type ContentCollectionDefinition,
5
- crossTenantOverrideDenied,
6
5
  defineQueryHandler,
7
6
  } from "@cosmicdrift/kumiko-framework/engine";
8
7
  import * as z from "zod";
@@ -36,12 +35,6 @@ export function makeCollectionListQuery(collection: ContentCollectionDefinition)
36
35
  : `Lists the tenant-wide entries of the "${collection.id}" content collection (${collection.kind} content) with slug, locale, title and body; every caller who may reach this collection sees the same shared set.`,
37
36
  handler: async (query, ctx) => {
38
37
  const override = query.payload.tenantIdOverride;
39
- const overrideDenied = crossTenantOverrideDenied(
40
- query.user,
41
- override,
42
- "templateResolver.errors.tenantOverrideRequiresSystemAdmin",
43
- );
44
- if (overrideDenied) throw overrideDenied;
45
38
  const tenantId = override ?? query.user.tenantId;
46
39
  const rows = castTenantRows<CollectionEntryRow>(
47
40
  await selectMany(ctx.db, store.table, {
@@ -1,11 +1,9 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import {
3
3
  type ContentCollectionDefinition,
4
- crossTenantOverrideDenied,
5
4
  defineWriteHandler,
6
5
  type TenantId,
7
6
  } from "@cosmicdrift/kumiko-framework/engine";
8
- import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
7
  import * as z from "zod";
10
8
  import {
11
9
  type CollectionEntryRow,
@@ -43,12 +41,6 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
43
41
  handler: async (event, ctx) => {
44
42
  const db = ctx.db;
45
43
  const override = event.payload.tenantIdOverride;
46
- const overrideDenied = crossTenantOverrideDenied(
47
- event.user,
48
- override,
49
- "templateResolver.errors.tenantOverrideRequiresSystemAdmin",
50
- );
51
- if (overrideDenied) return writeFailure(overrideDenied);
52
44
  // @cast-boundary engine-payload — override is a zod-validated string, the
53
45
  // user's own tenantId is already TenantId-branded.
54
46
  const tenantId = (override ?? event.user.tenantId) as TenantId;
@@ -1,11 +1,9 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import {
3
- crossTenantOverrideDenied,
4
3
  defineWriteHandler,
5
4
  SYSTEM_TENANT_ID,
6
5
  type TenantId,
7
6
  } from "@cosmicdrift/kumiko-framework/engine";
8
- import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
7
  import * as z from "zod";
10
8
  import { TEMPLATE_KINDS, TEXT_BLOCK_KIND } from "../constants.js";
11
9
  import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
@@ -52,12 +50,6 @@ export const setWrite = defineWriteHandler({
52
50
  handler: async (event, ctx) => {
53
51
  const db = ctx.db;
54
52
  const override = event.payload.tenantIdOverride;
55
- const overrideDenied = crossTenantOverrideDenied(
56
- event.user,
57
- override,
58
- "templateResolver.errors.tenantOverrideRequiresSystemAdmin",
59
- );
60
- if (overrideDenied) return writeFailure(overrideDenied);
61
53
  // @cast-boundary engine-payload — override is a zod-validated string, the
62
54
  // user's own tenantId is already TenantId-branded.
63
55
  const tenantId = (override ?? event.user.tenantId) as TenantId;
@@ -1,6 +1,5 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import {
3
- crossTenantOverrideDenied,
4
3
  defineWriteHandler,
5
4
  SYSTEM_TENANT_ID,
6
5
  type TenantId,
@@ -28,12 +27,6 @@ export const upsertTenantWrite = defineWriteHandler({
28
27
  handler: async (event, ctx) => {
29
28
  const db = ctx.db;
30
29
  const override = event.payload.tenantIdOverride;
31
- const overrideDenied = crossTenantOverrideDenied(
32
- event.user,
33
- override,
34
- "templateResolver.errors.tenantOverrideRequiresSystemAdmin",
35
- );
36
- if (overrideDenied) return writeFailure(overrideDenied);
37
30
  // upsertTenant erzeugt scope='tenant'. SYSTEM_TENANT_ID-Override würde
38
31
  // scope='tenant' unter SYSTEM_TENANT_ID schreiben → inkonsistenter Zustand
39
32
  // (Resolver-Logik trennt sauber zwischen system+tenant). SystemAdmin muss
@@ -51,8 +51,7 @@ async function resolveExistingForEventStoreSeed(
51
51
  `seed orphan check: projection ${id} has no events for tenant ${tenantId} but unscoped stream version is ${unscoped} — refusing delete/recreate to avoid wiping a live stream`,
52
52
  );
53
53
  }
54
- // @cast-boundary orphan projection cleanup: empty stream, no executor verb;
55
- // plain table ident trips guard-direct-entity-writes (infra EXCLUDE follow-up).
54
+ // The cast strips the executor-only brand that WritableTable rejects; the orphan row has no events, so no executor verb applies.
56
55
  await deleteMany(tdb, templateResourcesTable as EntityTableMeta, { id, tenantId });
57
56
  return null;
58
57
  }
@@ -155,6 +155,7 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
155
155
  fileProviderResolver: ctx._fileProviderResolver,
156
156
  searchAdapter: ctx.searchAdapter,
157
157
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
158
+ escapeHatchAuditLog: ctx.log,
158
159
  actor: ctx.systemUser.id,
159
160
  });
160
161
  },
@@ -14,6 +14,7 @@ import {
14
14
  VersionConflictError,
15
15
  } from "@cosmicdrift/kumiko-framework/event-store";
16
16
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
17
+ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
17
18
  import type { SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
18
19
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
19
20
  import { tenantEntity, tenantTable } from "../tenant/index.js";
@@ -283,6 +284,7 @@ export async function runNextDestructionStage(args: {
283
284
  readonly fileProviderResolver?: FileProviderResolver;
284
285
  readonly searchAdapter?: SearchAdapter;
285
286
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
287
+ readonly escapeHatchAuditLog?: Logger;
286
288
  readonly actor?: string;
287
289
  }): Promise<StageTickResult> {
288
290
  const events = await loadAggregate(args.db, args.tenantId, args.tenantId);
@@ -308,6 +310,7 @@ export async function runNextDestructionStage(args: {
308
310
  fileProviderResolver: args.fileProviderResolver,
309
311
  searchAdapter: args.searchAdapter,
310
312
  escapeHatchAuditSink: args.escapeHatchAuditSink,
313
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
311
314
  actor: args.actor,
312
315
  deadlineAt: Date.now() + DESTRUCTION_STAGE_TICK_BUDGET_MS,
313
316
  };
@@ -410,6 +413,7 @@ export async function runTenantDestructionSweep(args: {
410
413
  readonly fileProviderResolver?: FileProviderResolver;
411
414
  readonly searchAdapter?: SearchAdapter;
412
415
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
416
+ readonly escapeHatchAuditLog?: Logger;
413
417
  readonly actor?: string;
414
418
  }): Promise<{ readonly triggered: number; readonly advanced: number }> {
415
419
  const T = getTemporal();
@@ -457,6 +461,7 @@ export async function runTenantDestructionSweep(args: {
457
461
  fileProviderResolver: args.fileProviderResolver,
458
462
  searchAdapter: args.searchAdapter,
459
463
  escapeHatchAuditSink: args.escapeHatchAuditSink,
464
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
460
465
  actor: args.actor,
461
466
  });
462
467
  if (!result.error && !result.halted) advanced++;
@@ -27,6 +27,7 @@ import {
27
27
  type TenantResourceExtensionName,
28
28
  } from "@cosmicdrift/kumiko-framework/engine";
29
29
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
30
+ import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
30
31
  import {
31
32
  createEscapeHatchReporter,
32
33
  UNATTRIBUTED_ACTOR,
@@ -61,6 +62,7 @@ export type DestructionStageCtx = {
61
62
  // systemUser.id); runTenantDataHooks uses them to attribute+audit any
62
63
  // EXT_TENANT_DATA usage's declared escapeHatch.
63
64
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
65
+ readonly escapeHatchAuditLog?: Logger;
64
66
  readonly actor?: string;
65
67
  // Epoch ms the stage must hand back by returning { done: false }.
66
68
  readonly deadlineAt: number;
@@ -136,6 +138,7 @@ async function runTenantDataHooks(ctx: DestructionStageCtx): Promise<StageRunOut
136
138
  tenantId: ctx.tenantId,
137
139
  actor: ctx.actor ?? UNATTRIBUTED_ACTOR,
138
140
  sink: ctx.escapeHatchAuditSink,
141
+ log: ctx.escapeHatchAuditLog ?? createFallbackLogger("tenant-lifecycle"),
139
142
  });
140
143
  const hookCtx: TenantDataHookCtx = {
141
144
  db: createTenantDb(ctx.db, ctx.tenantId, "tenant", undefined, undefined, undefined, {
@@ -663,6 +663,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
663
663
  appExportDownloadUrl: opts.appExportDownloadUrl,
664
664
  }),
665
665
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
666
+ escapeHatchAuditLog: ctx.log,
666
667
  actor: ctx.systemUser.id,
667
668
  metrics: exportMetrics,
668
669
  });
@@ -732,6 +733,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
732
733
  }),
733
734
  ...(sendDeletionExecutedEmail && { sendDeletionExecutedEmail }),
734
735
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
736
+ escapeHatchAuditLog: ctx.log,
735
737
  actor: ctx.systemUser.id,
736
738
  });
737
739
 
@@ -1,3 +1,4 @@
1
+ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
1
2
  // Async Export-Job Worker (S2.U3 Atom 3b) — pure Pipeline-Function.
2
3
  //
3
4
  // Spec: docs/plans/architecture/user-data-rights.md "Async Export-Pipeline".
@@ -176,6 +177,7 @@ export interface RunExportJobsArgs {
176
177
  // fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
177
178
  // systemUser.id); threaded into runUserExport's per-hook TenantDb.
178
179
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
180
+ readonly escapeHatchAuditLog?: Logger;
179
181
  readonly actor?: string;
180
182
 
181
183
  /**
@@ -248,6 +250,7 @@ export async function runExportJobs(args: RunExportJobsArgs): Promise<RunExportJ
248
250
  now,
249
251
  job,
250
252
  escapeHatchAuditSink: args.escapeHatchAuditSink,
253
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
251
254
  actor: args.actor,
252
255
  });
253
256
  if (outcome.kind === "done") {
@@ -369,6 +372,7 @@ async function processJob(args: {
369
372
  now: Instant;
370
373
  job: JobRow;
371
374
  escapeHatchAuditSink?: EscapeHatchAuditSink;
375
+ escapeHatchAuditLog?: Logger;
372
376
  actor?: string;
373
377
  }): Promise<ProcessOutcome> {
374
378
  const { db, registry, buildStorageProvider, now, job } = args;
@@ -415,6 +419,7 @@ async function processJob(args: {
415
419
  userId: job.userId,
416
420
  now,
417
421
  escapeHatchAuditSink: args.escapeHatchAuditSink,
422
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
418
423
  actor: args.actor,
419
424
  });
420
425
 
@@ -1,3 +1,4 @@
1
+ import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
1
2
  // Forget-Cleanup-Runner (S2.U5b) — pure-Function Pipeline.
2
3
  //
3
4
  // After the grace period (S2.U5a sets gracePeriodEnd) this runner iterates
@@ -157,6 +158,7 @@ export interface RunForgetCleanupArgs {
157
158
  // systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
158
159
  // escapeHatch when its delete hook calls ctx.db.unsafeRaw().
159
160
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
161
+ readonly escapeHatchAuditLog?: Logger;
160
162
  readonly actor?: string;
161
163
  }
162
164
 
@@ -209,13 +211,18 @@ function buildHookDb(
209
211
  db: DbRunner,
210
212
  tenantId: TenantId,
211
213
  entry: HookEntry,
212
- args: { readonly escapeHatchAuditSink?: EscapeHatchAuditSink; readonly actor?: string },
214
+ args: {
215
+ readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
216
+ readonly escapeHatchAuditLog?: Logger;
217
+ readonly actor?: string;
218
+ },
213
219
  ) {
214
220
  const report = createEscapeHatchReporter({
215
221
  handler: `${EXT_USER_DATA}:${entry.entityName}`,
216
222
  tenantId,
217
223
  actor: args.actor ?? UNATTRIBUTED_ACTOR,
218
224
  sink: args.escapeHatchAuditSink,
225
+ log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
219
226
  });
220
227
  return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
221
228
  unsafeRaw:
@@ -296,6 +303,7 @@ export async function runForgetCleanup(
296
303
  kms,
297
304
  searchAdapter: args.searchAdapter,
298
305
  escapeHatchAuditSink: args.escapeHatchAuditSink,
306
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
299
307
  actor: args.actor,
300
308
  });
301
309
  hookCallsAttempted += userResult.hookCallsAttempted;
@@ -362,6 +370,7 @@ async function processUser(args: {
362
370
  kms?: KmsAdapter;
363
371
  searchAdapter?: SearchAdapter;
364
372
  escapeHatchAuditSink?: EscapeHatchAuditSink;
373
+ escapeHatchAuditLog?: Logger;
365
374
  actor?: string;
366
375
  }): Promise<ProcessUserResult> {
367
376
  const { db, registry, userId, hookEntries, buildStorageProvider, appTenantModel, kms } = args;
@@ -460,6 +469,7 @@ async function processUser(args: {
460
469
  {
461
470
  db: buildHookDb(tx, tenantId, entry, {
462
471
  escapeHatchAuditSink: args.escapeHatchAuditSink,
472
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
463
473
  actor: args.actor,
464
474
  }),
465
475
  registry,
@@ -1,3 +1,4 @@
1
+ import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
1
2
  // User-Data-Export-Pipeline (S2.U3) — DSGVO Art. 15 (Auskunft) +
2
3
  // Art. 20 (Datenportabilität).
3
4
  //
@@ -62,6 +63,7 @@ export interface RunUserExportArgs {
62
63
  // systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
63
64
  // escapeHatch when its export hook calls ctx.db.unsafeRaw().
64
65
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
66
+ readonly escapeHatchAuditLog?: Logger;
65
67
  readonly actor?: string;
66
68
  }
67
69
 
@@ -110,13 +112,14 @@ function buildHookDb(
110
112
  db: DbRunner,
111
113
  tenantId: TenantId,
112
114
  entry: HookEntry,
113
- args: Pick<RunUserExportArgs, "escapeHatchAuditSink" | "actor">,
115
+ args: Pick<RunUserExportArgs, "escapeHatchAuditSink" | "escapeHatchAuditLog" | "actor">,
114
116
  ) {
115
117
  const report = createEscapeHatchReporter({
116
118
  handler: `${EXT_USER_DATA}:${entry.entityName}`,
117
119
  tenantId,
118
120
  actor: args.actor ?? UNATTRIBUTED_ACTOR,
119
121
  sink: args.escapeHatchAuditSink,
122
+ log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
120
123
  });
121
124
  return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
122
125
  unsafeRaw: