@cosmicdrift/kumiko-bundled-features 0.354.1 → 0.356.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 (48) hide show
  1. package/dist/agent-tools/agent-manifest.js +2 -0
  2. package/dist/agent-tools/types.d.ts +2 -0
  3. package/dist/audit/escape-hatch-audit-sink.d.ts +1 -0
  4. package/dist/audit/escape-hatch-audit-sink.js +2 -0
  5. package/dist/audit/handlers/resolve-audit-tenant.js +8 -4
  6. package/dist/billing-foundation/consumer-protection/register-consent.js +1 -2
  7. package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
  8. package/dist/billing-foundation/consumer-protection/register-termination.js +1 -2
  9. package/dist/cap-overview/handlers/caps-usage.query.js +8 -5
  10. package/dist/compliance-profiles/handlers/set-profile.write.js +15 -26
  11. package/dist/custom-fields/handlers/define-system-field.write.js +8 -16
  12. package/dist/custom-fields/handlers/delete-system-field.write.js +5 -2
  13. package/dist/custom-fields/lib/define-or-resurrect.d.ts +2 -1
  14. package/dist/custom-fields/lib/define-or-resurrect.js +11 -4
  15. package/dist/delivery/delivery-service.js +1 -1
  16. package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -4
  17. package/dist/managed-pages/handlers/set.write.js +7 -22
  18. package/dist/shared/index.d.ts +1 -0
  19. package/dist/shared/index.js +1 -0
  20. package/dist/shared/require-for-tenant.d.ts +2 -0
  21. package/dist/shared/require-for-tenant.js +9 -0
  22. package/dist/template-resolver/handlers/by-slug.query.js +1 -4
  23. package/dist/template-resolver/handlers/by-tenant.query.js +1 -4
  24. package/dist/template-resolver/handlers/collection-item.query.js +1 -4
  25. package/dist/template-resolver/handlers/collection-list.query.js +1 -4
  26. package/dist/template-resolver/handlers/collection-set.write.js +5 -9
  27. package/dist/template-resolver/handlers/set.write.js +7 -12
  28. package/dist/template-resolver/handlers/upsert-system.write.js +6 -6
  29. package/dist/template-resolver/handlers/upsert-tenant.write.js +7 -8
  30. package/dist/template-resolver/seeding.js +1 -2
  31. package/dist/tenant/cancel-pending-invitation.d.ts +2 -2
  32. package/dist/tenant/cancel-pending-invitation.js +2 -2
  33. package/dist/tenant/handlers/remove-member.write.js +13 -10
  34. package/dist/tenant/handlers/update-member-roles.write.js +12 -9
  35. package/dist/tenant-lifecycle/feature.js +1 -0
  36. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +3 -0
  37. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -0
  38. package/dist/tenant-lifecycle/stages.d.ts +2 -0
  39. package/dist/tenant-lifecycle/stages.js +2 -0
  40. package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -18
  41. package/dist/user-data-rights/feature.js +2 -0
  42. package/dist/user-data-rights/run-export-jobs.d.ts +2 -0
  43. package/dist/user-data-rights/run-export-jobs.js +2 -0
  44. package/dist/user-data-rights/run-forget-cleanup.d.ts +2 -0
  45. package/dist/user-data-rights/run-forget-cleanup.js +4 -0
  46. package/dist/user-data-rights/run-user-export.d.ts +2 -0
  47. package/dist/user-data-rights/run-user-export.js +2 -0
  48. package/package.json +9 -9
@@ -1,7 +1,7 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { crossTenantOverrideDenied, defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
- import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
2
+ import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
4
3
  import * as z from "zod";
4
+ import { requireForTenant } from "../../shared/index.js";
5
5
  import { TEMPLATE_KINDS, TEXT_BLOCK_KIND } from "../constants.js";
6
6
  import { templateResourcesTable } from "../table.js";
7
7
  import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema } from "./shared.js";
@@ -43,18 +43,11 @@ export const setWrite = defineWriteHandler({
43
43
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
44
44
  description: "Creates or overwrites one content-tree resource addressed by slug, kind and locale, carrying a title and folder and going live on save with no draft stage; a SystemAdmin may target the system tenant, where shared legal and marketing copy lives.",
45
45
  handler: async (event, ctx) => {
46
- const db = ctx.db;
47
46
  const override = event.payload.tenantIdOverride;
48
- const overrideDenied = crossTenantOverrideDenied(event.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
49
- if (overrideDenied)
50
- return writeFailure(overrideDenied);
51
47
  // @cast-boundary engine-payload — override is a zod-validated string, the
52
48
  // user's own tenantId is already TenantId-branded.
53
49
  const tenantId = (override ?? event.user.tenantId);
54
- // With an override the executor user has to move to the target tenant too,
55
- // otherwise the event-store stream lookup runs against event.user.tenantId
56
- // and reports a version conflict although the projection row exists.
57
- const executorUser = override !== undefined ? { ...event.user, tenantId } : event.user;
50
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
58
51
  const kind = event.payload.kind ?? TEXT_BLOCK_KIND;
59
52
  const existing = await fetchOne(db, templateResourcesTable, {
60
53
  tenantId,
@@ -91,7 +84,7 @@ export const setWrite = defineWriteHandler({
91
84
  contentFormat: fields.contentFormat,
92
85
  folder: fields.folder,
93
86
  },
94
- }, executorUser, db);
87
+ }, event.user, db, { streamTenantId });
95
88
  if (!result.isSuccess)
96
89
  return result;
97
90
  return {
@@ -99,7 +92,9 @@ export const setWrite = defineWriteHandler({
99
92
  data: { slug: event.payload.slug, locale: event.payload.locale, isNew: false },
100
93
  };
101
94
  }
102
- const result = await executor.create({ ...fields, tenantId }, executorUser, db);
95
+ const result = await executor.create({ ...fields, tenantId }, event.user, db, {
96
+ streamTenantId,
97
+ });
103
98
  if (!result.isSuccess)
104
99
  return result;
105
100
  return {
@@ -1,5 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { requireForTenant } from "../../shared/index.js";
3
4
  import { templateResourcesTable } from "../table.js";
4
5
  import { executor, upsertPayloadSchema } from "./shared.js";
5
6
  // System-Template anlegen/updaten. Liegt unter SYSTEM_TENANT_ID,
@@ -12,13 +13,10 @@ export const upsertSystemWrite = defineWriteHandler({
12
13
  access: { roles: ["SystemAdmin"] },
13
14
  description: "Creates or overwrites a system-default template under the system tenant with its full variable schema and linked resources, active straight away with no draft stage; SystemAdmin only, since tenants shadow these defaults through upsert-tenant instead.",
14
15
  handler: async (event, ctx) => {
15
- const db = ctx.db;
16
16
  // @cast-boundary engine-payload — SYSTEM_TENANT_ID is a UUID literal,
17
17
  // asserted to the branded TenantId (parseTenantId equivalent).
18
18
  const tenantId = SYSTEM_TENANT_ID;
19
- // The executor user must carry SYSTEM_TENANT as tenantId, otherwise the
20
- // event store looks up the stream under user.tenantId → conflict.
21
- const executorUser = { ...event.user, tenantId };
19
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
22
20
  const existing = await fetchOne(db, templateResourcesTable, {
23
21
  tenantId,
24
22
  slug: event.payload.slug,
@@ -41,7 +39,7 @@ export const upsertSystemWrite = defineWriteHandler({
41
39
  status: "active",
42
40
  };
43
41
  if (existing) {
44
- const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, executorUser, db);
42
+ const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, event.user, db, { streamTenantId });
45
43
  if (!result.isSuccess)
46
44
  return result;
47
45
  return {
@@ -49,7 +47,9 @@ export const upsertSystemWrite = defineWriteHandler({
49
47
  data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
50
48
  };
51
49
  }
52
- const result = await executor.create({ ...fields, tenantId }, executorUser, db);
50
+ const result = await executor.create({ ...fields, tenantId }, event.user, db, {
51
+ streamTenantId,
52
+ });
53
53
  if (!result.isSuccess)
54
54
  return result;
55
55
  // @cast-boundary db-row — executor.create returnt Record-row aus
@@ -1,7 +1,8 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { crossTenantOverrideDenied, defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { AccessDeniedError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import * as z from "zod";
5
+ import { requireForTenant } from "../../shared/index.js";
5
6
  import { templateResourcesTable } from "../table.js";
6
7
  import { executor, upsertPayloadSchema } from "./shared.js";
7
8
  // Tenant-Override anlegen/updaten. Liegt unter event.user.tenantId,
@@ -18,11 +19,7 @@ export const upsertTenantWrite = defineWriteHandler({
18
19
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
19
20
  description: "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live; it refuses a system-tenant target, which is what upsert-system is for.",
20
21
  handler: async (event, ctx) => {
21
- const db = ctx.db;
22
22
  const override = event.payload.tenantIdOverride;
23
- const overrideDenied = crossTenantOverrideDenied(event.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
24
- if (overrideDenied)
25
- return writeFailure(overrideDenied);
26
23
  // upsertTenant erzeugt scope='tenant'. SYSTEM_TENANT_ID-Override würde
27
24
  // scope='tenant' unter SYSTEM_TENANT_ID schreiben → inkonsistenter Zustand
28
25
  // (Resolver-Logik trennt sauber zwischen system+tenant). SystemAdmin muss
@@ -37,7 +34,7 @@ export const upsertTenantWrite = defineWriteHandler({
37
34
  // event.user.tenantId schon TenantId-branded; union als TenantId casten
38
35
  // ist legit (override ist UUID-Format-validiert in schema).
39
36
  const tenantId = (override ?? event.user.tenantId);
40
- const executorUser = override !== undefined ? { ...event.user, tenantId } : event.user;
37
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
41
38
  const existing = await fetchOne(db, templateResourcesTable, {
42
39
  tenantId,
43
40
  slug: event.payload.slug,
@@ -59,7 +56,7 @@ export const upsertTenantWrite = defineWriteHandler({
59
56
  status: event.payload.status,
60
57
  };
61
58
  if (existing) {
62
- const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, executorUser, db);
59
+ const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, event.user, db, { streamTenantId });
63
60
  if (!result.isSuccess)
64
61
  return result;
65
62
  return {
@@ -67,7 +64,9 @@ export const upsertTenantWrite = defineWriteHandler({
67
64
  data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
68
65
  };
69
66
  }
70
- const result = await executor.create({ ...fields, tenantId }, executorUser, db);
67
+ const result = await executor.create({ ...fields, tenantId }, event.user, db, {
68
+ streamTenantId,
69
+ });
71
70
  if (!result.isSuccess)
72
71
  return result;
73
72
  // @cast-boundary db-row — executor.create returnt Record-row aus
@@ -29,8 +29,7 @@ async function resolveExistingForEventStoreSeed(db, tdb, existingRow, tenantId)
29
29
  if (unscoped !== 0) {
30
30
  throw new Error(`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`);
31
31
  }
32
- // @cast-boundary orphan projection cleanup: empty stream, no executor verb;
33
- // plain table ident trips guard-direct-entity-writes (infra EXCLUDE follow-up).
32
+ // The cast strips the executor-only brand that WritableTable rejects; the orphan row has no events, so no executor verb applies.
34
33
  await deleteMany(tdb, templateResourcesTable, { id, tenantId });
35
34
  return null;
36
35
  }
@@ -1,8 +1,8 @@
1
1
  import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
- import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import type { Redis } from "ioredis";
5
5
  export declare function cancelPendingInvitation(db: TenantDb, invitation: {
6
6
  readonly id: string;
7
7
  readonly version: number;
8
- }, actor: SessionUser, redis: Redis | undefined): Promise<WriteFailure | undefined>;
8
+ }, actor: SessionUser, redis: Redis | undefined, streamTenantId?: TenantId): Promise<WriteFailure | undefined>;
@@ -5,12 +5,12 @@ import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } fr
5
5
  const invitationExecutor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, { entityName: "tenant-invitation" });
6
6
  // Shared by cancel-invitation and remove-member so the cancel behavior
7
7
  // (status transition + token invalidation) cannot drift between them.
8
- export async function cancelPendingInvitation(db, invitation, actor, redis) {
8
+ export async function cancelPendingInvitation(db, invitation, actor, redis, streamTenantId) {
9
9
  const updateResult = await invitationExecutor.update({
10
10
  id: invitation.id,
11
11
  version: invitation.version,
12
12
  changes: { status: INVITATION_STATUS.cancelled },
13
- }, actor, db);
13
+ }, actor, db, { streamTenantId });
14
14
  if (!updateResult.isSuccess)
15
15
  return updateResult;
16
16
  // Delete the token from Redis (if still there). If Redis is
@@ -1,6 +1,6 @@
1
1
  import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createEventStoreExecutor, } from "@cosmicdrift/kumiko-framework/db";
3
- import { createSystemUser, defineWriteHandler, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createSystemUser, defineWriteHandler, parseTenantId, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { InternalError, NotFoundError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
5
5
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
6
6
  import * as z from "zod";
@@ -20,7 +20,7 @@ async function cancelPendingInvitationsOfUser(db, options) {
20
20
  const email = (await decryptStoredPii(user.email, "email", "tenant:remove-member")).toLowerCase();
21
21
  const pendingInvitations = await selectMany(db, tenantInvitationsTable, { tenantId: options.tenantId, email, status: INVITATION_STATUS.pending });
22
22
  for (const invitation of pendingInvitations) {
23
- const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis);
23
+ const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis, options.streamTenantId);
24
24
  if (failure)
25
25
  return failure;
26
26
  }
@@ -43,11 +43,14 @@ export const removeMemberWrite = defineWriteHandler({
43
43
  });
44
44
  }
45
45
  const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin manages memberships across tenants");
46
- const existing = await fetchOne(db, tenantMembershipsTable, {
47
- userId: event.payload.userId,
48
- tenantId: event.payload.tenantId,
49
- });
50
- if (!existing) {
46
+ const streamTenantId = parseTenantId(event.payload.tenantId);
47
+ const existing = streamTenantId === null
48
+ ? undefined
49
+ : await fetchOne(db, tenantMembershipsTable, {
50
+ userId: event.payload.userId,
51
+ tenantId: streamTenantId,
52
+ });
53
+ if (!existing || streamTenantId === null) {
51
54
  return writeFailure(new NotFoundError("membership", undefined, {
52
55
  i18nKey: "tenant.errors.membershipNotFound",
53
56
  i18nParams: { userId: event.payload.userId, tenantId: event.payload.tenantId },
@@ -75,14 +78,14 @@ export const removeMemberWrite = defineWriteHandler({
75
78
  });
76
79
  }
77
80
  const result = await executor.delete({ id: existing["id"] }, // @cast-boundary db-row
78
- event.user, db);
81
+ event.user, db, { streamTenantId });
79
82
  if (!result.isSuccess)
80
83
  return result;
81
- // Actor tenant = invitation tenant so the update hits the invitation's stream.
82
84
  const cancelFailure = await cancelPendingInvitationsOfUser(db, {
83
85
  userId: event.payload.userId,
84
86
  tenantId: event.payload.tenantId,
85
- actor: { ...event.user, tenantId: event.payload.tenantId },
87
+ actor: event.user,
88
+ streamTenantId,
86
89
  redis: ctx.redis,
87
90
  });
88
91
  if (cancelFailure !== undefined)
@@ -1,6 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
- import { access, assignableAppRolesOf, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, mergeAssignedRoles, SYSTEM_ROLE, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { access, assignableAppRolesOf, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, mayOverrideTenant, mergeAssignedRoles, parseTenantId, SYSTEM_ROLE, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { InternalError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
5
5
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
6
6
  import * as z from "zod";
@@ -17,7 +17,12 @@ const REVOKE_ALL_SESSIONS_QN = "sessions:write:user-session:revoke-all-for-user"
17
17
  /** SystemAdmin may pass payload.tenantId (cross-tenant); the members actionForm
18
18
  * only sends userId+roles, so fall back to the active session tenant. */
19
19
  function resolveTargetTenantId(isSystem, payloadTenantId, sessionTenantId) {
20
- return isSystem ? (payloadTenantId ?? sessionTenantId) : sessionTenantId;
20
+ if (!isSystem || payloadTenantId === undefined)
21
+ return sessionTenantId;
22
+ return parseTenantId(payloadTenantId) ?? undefined;
23
+ }
24
+ function foreignStreamTenant(user, targetTenantId) {
25
+ return targetTenantId === user.tenantId ? undefined : targetTenantId;
21
26
  }
22
27
  // Editing your OWN roles keeps the current session: roles are re-derived per
23
28
  // request, so the new roles apply at once without a re-login. Other sessions
@@ -49,7 +54,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
49
54
  });
50
55
  }
51
56
  const isSystemActor = event.user.roles.includes(SYSTEM_ROLE);
52
- const isSystem = event.user.roles.includes("SystemAdmin") || isSystemActor;
57
+ const isSystem = mayOverrideTenant(event.user) || isSystemActor;
53
58
  const assignableAppRoles = assignableAppRolesOf(ctx.registry);
54
59
  const targetTenantId = resolveTargetTenantId(isSystem, event.payload.tenantId, event.user.tenantId);
55
60
  if (!targetTenantId) {
@@ -117,16 +122,14 @@ export const updateMemberRolesWrite = defineWriteHandler({
117
122
  if (revoker) {
118
123
  await ctx.writeAs(createSystemUser(targetTenantId), REVOKE_ALL_SESSIONS_QN, revokeSessionsPayload(event.payload.userId, targetTenantId, event.user));
119
124
  }
120
- // Stream tenant follows the actor via streamTenantFor — use the
121
- // membership tenant so SystemAdmin/cross-tenant and TenantAdmin paths
122
- // hit the same stream seedTenantMembership/invite-accept wrote.
123
- // Keep the real actor for audit metadata; only override tenantId so
124
- // streamTenantFor writes into the membership tenant (#2401).
125
+ // The membership stream lives on the membership's tenant (where seedTenantMembership/
126
+ // invite-accept wrote it); a cross-tenant operator addresses it without becoming a member.
127
+ // Same-tenant writes run on a tenant-mode db, which refuses a stream override.
125
128
  const result = await executor.update({
126
129
  id: row["id"], // @cast-boundary db-row
127
130
  version: row["version"], // @cast-boundary db-row
128
131
  changes: { roles: JSON.stringify(nextRoles) },
129
- }, { ...event.user, tenantId: targetTenantId }, db);
132
+ }, event.user, db, { streamTenantId: foreignStreamTenant(event.user, targetTenantId) });
130
133
  return withResponseData(result, {
131
134
  ...event.payload,
132
135
  roles: nextRoles,
@@ -89,6 +89,7 @@ export function createTenantLifecycleFeature() {
89
89
  fileProviderResolver: ctx._fileProviderResolver,
90
90
  searchAdapter: ctx.searchAdapter,
91
91
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
92
+ escapeHatchAuditLog: ctx.log,
92
93
  actor: ctx.systemUser.id,
93
94
  });
94
95
  },
@@ -2,6 +2,7 @@
2
2
  import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { type EscapeHatchAuditSink, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
5
+ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
5
6
  import type { SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
6
7
  type StageTickResult = {
7
8
  readonly done: boolean;
@@ -17,6 +18,7 @@ export declare function runNextDestructionStage(args: {
17
18
  readonly fileProviderResolver?: FileProviderResolver;
18
19
  readonly searchAdapter?: SearchAdapter;
19
20
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
21
+ readonly escapeHatchAuditLog?: Logger;
20
22
  readonly actor?: string;
21
23
  }): Promise<StageTickResult>;
22
24
  export declare function hasTenantDestructionWork(reads: Pick<TenantDb, "count">, now: Temporal.Instant): Promise<boolean>;
@@ -29,6 +31,7 @@ export declare function runTenantDestructionSweep(args: {
29
31
  readonly fileProviderResolver?: FileProviderResolver;
30
32
  readonly searchAdapter?: SearchAdapter;
31
33
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
34
+ readonly escapeHatchAuditLog?: Logger;
32
35
  readonly actor?: string;
33
36
  }): Promise<{
34
37
  readonly triggered: number;
@@ -162,6 +162,7 @@ export async function runNextDestructionStage(args) {
162
162
  fileProviderResolver: args.fileProviderResolver,
163
163
  searchAdapter: args.searchAdapter,
164
164
  escapeHatchAuditSink: args.escapeHatchAuditSink,
165
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
165
166
  actor: args.actor,
166
167
  deadlineAt: Date.now() + DESTRUCTION_STAGE_TICK_BUDGET_MS,
167
168
  };
@@ -258,6 +259,7 @@ export async function runTenantDestructionSweep(args) {
258
259
  fileProviderResolver: args.fileProviderResolver,
259
260
  searchAdapter: args.searchAdapter,
260
261
  escapeHatchAuditSink: args.escapeHatchAuditSink,
262
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
261
263
  actor: args.actor,
262
264
  });
263
265
  if (!result.error && !result.halted)
@@ -1,6 +1,7 @@
1
1
  import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { type EscapeHatchAuditSink, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
4
+ import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
4
5
  import { type SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
5
6
  import type { TenantDestructionStageName } from "./constants.js";
6
7
  export type DestructionStageCtx = {
@@ -11,6 +12,7 @@ export type DestructionStageCtx = {
11
12
  readonly fileProviderResolver?: FileProviderResolver;
12
13
  readonly searchAdapter?: SearchAdapter;
13
14
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
15
+ readonly escapeHatchAuditLog?: Logger;
14
16
  readonly actor?: string;
15
17
  readonly deadlineAt: number;
16
18
  };
@@ -2,6 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { configuredPiiSubjectKms, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
3
3
  import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
4
4
  import { createSystemUser, EXT_EXTERNAL_RESOURCE, EXT_INFRA_RESOURCE, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, extensionUsageEscapeHatchReason, isTenantDataExtensionHooks, isTenantResourceExtensionHooks, } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
5
6
  import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
6
7
  import { purgeSearchDocumentsForSubject, } from "@cosmicdrift/kumiko-framework/search";
7
8
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
@@ -51,6 +52,7 @@ async function runTenantDataHooks(ctx) {
51
52
  tenantId: ctx.tenantId,
52
53
  actor: ctx.actor ?? UNATTRIBUTED_ACTOR,
53
54
  sink: ctx.escapeHatchAuditSink,
55
+ log: ctx.escapeHatchAuditLog ?? createFallbackLogger("tenant-lifecycle"),
54
56
  });
55
57
  const hookCtx = {
56
58
  db: createTenantDb(ctx.db, ctx.tenantId, "tenant", undefined, undefined, undefined, {
@@ -1,7 +1,8 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { buildEntityTable, createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
2
+ import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import * as z from "zod";
5
+ import { requireForTenant } from "../../shared/index.js";
5
6
  import { tierAssignmentAggregateId } from "../aggregate-id.js";
6
7
  import { TierAssignmentSources } from "../constants.js";
7
8
  import { tierAssignmentEntity } from "../entity.js";
@@ -9,13 +10,8 @@ import { tierAssignmentEntity } from "../entity.js";
9
10
  // Billing. Cross-tenant, daher SystemAdmin-only (kein TenantAdmin: sonst
10
11
  // Gratis-Self-Upgrade).
11
12
  //
12
- // **Cross-tenant-Mechanik:** ein "system"-mode TenantDb auf den Ziel-Tenant legt
13
- // KEINEN Tenant-Filter an (tenant-db.ts:141 — mode==="system" überspringt ihn);
14
- // der executor-user wird ebenfalls auf den Ziel-Tenant gestellt, sonst landet das
15
- // Event im Stream des Admins (Memory feedback_event_store_tenant_consistency).
16
- // Das set.write-"override-user"-Muster trägt NICHT für beliebige Tenants — es
17
- // funktioniert nur für SYSTEM_TENANT_ID (immer im IN-Filter). Dies ist das
18
- // auto-default-Hook-Muster (feature.ts), generalisiert auf einen Request-Handler.
13
+ // Cross-tenant mechanics: ctx.forTenant(tenantId) yields a tenant-mode db bound to the target plus
14
+ // the streamTenantId, so the event lands in the target's stream with the operator as actor.
19
15
  //
20
16
  // `source: TierAssignmentSources.manual` marks the grant so the billing sync skips it.
21
17
  // Upsert: one aggregate per tenant (deterministic aggregate id).
@@ -31,7 +27,6 @@ const tierAssignmentTable = buildEntityTable("tier-assignment", tierAssignmentEn
31
27
  const executor = createEventStoreExecutor(tierAssignmentTable, tierAssignmentEntity, {
32
28
  entityName: "tier-assignment",
33
29
  });
34
- const SET_TENANT_TIER_REASON = "SystemAdmin assigns the tier of the tenant named in the payload";
35
30
  export function createSetTenantTierWrite(opts = {}) {
36
31
  return defineWriteHandler({
37
32
  name: "set-tenant-tier",
@@ -46,10 +41,6 @@ export function createSetTenantTierWrite(opts = {}) {
46
41
  }),
47
42
  access: { roles: ["SystemAdmin"] },
48
43
  agent: { risk: "high" },
49
- escapeHatch: {
50
- grants: ["unsafeRaw"],
51
- reason: SET_TENANT_TIER_REASON,
52
- },
53
44
  handler: async (event, ctx) => {
54
45
  // Without a commit sink (unit harness) there is no transaction to wait for.
55
46
  const afterCommit = (tenantId, tier) => {
@@ -60,9 +51,7 @@ export function createSetTenantTierWrite(opts = {}) {
60
51
  opts.onAssigned?.(tenantId, tier);
61
52
  };
62
53
  const tenantId = event.payload.tenantId; // @cast-boundary engine-bridge
63
- const rawDb = ctx.db.unsafeRaw();
64
- const tdb = createTenantDb(rawDb, tenantId, "system");
65
- const systemUser = { ...event.user, tenantId };
54
+ const { db: tdb, streamTenantId } = requireForTenant(ctx, tenantId);
66
55
  const tier = event.payload.tier;
67
56
  const existing = await fetchOne(tdb, tierAssignmentTable, { tenantId });
68
57
  if (existing) {
@@ -70,7 +59,7 @@ export function createSetTenantTierWrite(opts = {}) {
70
59
  id: existing.id,
71
60
  version: existing.version,
72
61
  changes: { tier, source: TierAssignmentSources.manual },
73
- }, systemUser, tdb);
62
+ }, event.user, tdb, { streamTenantId });
74
63
  if (!result.isSuccess)
75
64
  return result;
76
65
  afterCommit(tenantId, tier);
@@ -81,7 +70,7 @@ export function createSetTenantTierWrite(opts = {}) {
81
70
  tier,
82
71
  source: TierAssignmentSources.manual,
83
72
  tenantId,
84
- }, systemUser, tdb);
73
+ }, event.user, tdb, { streamTenantId });
85
74
  if (!result.isSuccess)
86
75
  return result;
87
76
  afterCommit(tenantId, tier);
@@ -477,6 +477,7 @@ export function createUserDataRightsFeature(opts = {}) {
477
477
  appExportDownloadUrl: opts.appExportDownloadUrl,
478
478
  }),
479
479
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
480
+ escapeHatchAuditLog: ctx.log,
480
481
  actor: ctx.systemUser.id,
481
482
  metrics: exportMetrics,
482
483
  });
@@ -541,6 +542,7 @@ export function createUserDataRightsFeature(opts = {}) {
541
542
  }),
542
543
  ...(sendDeletionExecutedEmail && { sendDeletionExecutedEmail }),
543
544
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
545
+ escapeHatchAuditLog: ctx.log,
544
546
  actor: ctx.systemUser.id,
545
547
  });
546
548
  // Persist each incomplete report on the job-run log (#1572). The write
@@ -1,3 +1,4 @@
1
+ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
1
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
3
  import type { EscapeHatchAuditSink, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { type FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
@@ -75,6 +76,7 @@ export interface RunExportJobsArgs {
75
76
  * Callback durch. Required wenn sendExportReadyEmail gesetzt. */
76
77
  readonly appExportDownloadUrl?: string;
77
78
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
79
+ readonly escapeHatchAuditLog?: Logger;
78
80
  readonly actor?: string;
79
81
  /**
80
82
  * Reports EXPORT_CLEANUP_BACKLOG_AGE_METRIC after the storage-cleanup
@@ -106,6 +106,7 @@ export async function runExportJobs(args) {
106
106
  now,
107
107
  job,
108
108
  escapeHatchAuditSink: args.escapeHatchAuditSink,
109
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
109
110
  actor: args.actor,
110
111
  });
111
112
  if (outcome.kind === "done") {
@@ -230,6 +231,7 @@ async function processJob(args) {
230
231
  userId: job.userId,
231
232
  now,
232
233
  escapeHatchAuditSink: args.escapeHatchAuditSink,
234
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
233
235
  actor: args.actor,
234
236
  });
235
237
  const profile = await resolveProfileForTenant({
@@ -1,3 +1,4 @@
1
+ import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
1
2
  import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
2
3
  import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
4
  import { type EscapeHatchAuditSink, type Registry, type TenantId, type TenantUserModel, type UserDataDeleteStrategy, type UserDataStorageProvider } from "@cosmicdrift/kumiko-framework/engine";
@@ -69,6 +70,7 @@ export interface RunForgetCleanupArgs {
69
70
  */
70
71
  readonly tenantModel?: TenantUserModel;
71
72
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
73
+ readonly escapeHatchAuditLog?: Logger;
72
74
  readonly actor?: string;
73
75
  }
74
76
  export interface ForgetCleanupError {
@@ -1,3 +1,4 @@
1
+ import { createFallbackLogger } 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
@@ -51,6 +52,7 @@ function buildHookDb(db, tenantId, entry, args) {
51
52
  tenantId,
52
53
  actor: args.actor ?? UNATTRIBUTED_ACTOR,
53
54
  sink: args.escapeHatchAuditSink,
55
+ log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
54
56
  });
55
57
  return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
56
58
  unsafeRaw: entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
@@ -117,6 +119,7 @@ export async function runForgetCleanup(args) {
117
119
  kms,
118
120
  searchAdapter: args.searchAdapter,
119
121
  escapeHatchAuditSink: args.escapeHatchAuditSink,
122
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
120
123
  actor: args.actor,
121
124
  });
122
125
  hookCallsAttempted += userResult.hookCallsAttempted;
@@ -235,6 +238,7 @@ async function processUser(args) {
235
238
  const hookResult = await entry.deleteHook({
236
239
  db: buildHookDb(tx, tenantId, entry, {
237
240
  escapeHatchAuditSink: args.escapeHatchAuditSink,
241
+ escapeHatchAuditLog: args.escapeHatchAuditLog,
238
242
  actor: args.actor,
239
243
  }),
240
244
  registry,
@@ -1,3 +1,4 @@
1
+ import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
1
2
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { type EscapeHatchAuditSink, type Registry, type TenantId, type UserDataExportSnippet } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
@@ -8,6 +9,7 @@ export interface RunUserExportArgs {
8
9
  readonly userId: string;
9
10
  readonly now: Instant;
10
11
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
12
+ readonly escapeHatchAuditLog?: Logger;
11
13
  readonly actor?: string;
12
14
  }
13
15
  export interface UserExportFileRef {
@@ -1,3 +1,4 @@
1
+ import { createFallbackLogger } 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
  //
@@ -32,6 +33,7 @@ function buildHookDb(db, tenantId, entry, args) {
32
33
  tenantId,
33
34
  actor: args.actor ?? UNATTRIBUTED_ACTOR,
34
35
  sink: args.escapeHatchAuditSink,
36
+ log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
35
37
  });
36
38
  return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
37
39
  unsafeRaw: entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.354.1",
3
+ "version": "0.356.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>",
@@ -516,12 +516,12 @@
516
516
  }
517
517
  },
518
518
  "dependencies": {
519
- "@cosmicdrift/kumiko-dispatcher-live": "0.354.1",
520
- "@cosmicdrift/kumiko-framework": "0.354.1",
521
- "@cosmicdrift/kumiko-headless": "0.354.1",
522
- "@cosmicdrift/kumiko-renderer": "0.354.1",
523
- "@cosmicdrift/kumiko-renderer-web": "0.354.1",
524
- "@cosmicdrift/kumiko-types": "0.354.1",
519
+ "@cosmicdrift/kumiko-dispatcher-live": "0.356.0",
520
+ "@cosmicdrift/kumiko-framework": "0.356.0",
521
+ "@cosmicdrift/kumiko-headless": "0.356.0",
522
+ "@cosmicdrift/kumiko-renderer": "0.356.0",
523
+ "@cosmicdrift/kumiko-renderer-web": "0.356.0",
524
+ "@cosmicdrift/kumiko-types": "0.356.0",
525
525
  "@mollie/api-client": "^4.5.0",
526
526
  "@node-rs/argon2": "^2.0.2",
527
527
  "@types/mailparser": "^3.4.6",
@@ -1050,8 +1050,8 @@
1050
1050
  ],
1051
1051
  "devDependencies": {
1052
1052
  "@testing-library/user-event": "^14.6.1",
1053
- "@cosmicdrift/kumiko-locale-de": "0.354.1",
1054
- "@cosmicdrift/kumiko-locale-es": "0.354.1",
1053
+ "@cosmicdrift/kumiko-locale-de": "0.356.0",
1054
+ "@cosmicdrift/kumiko-locale-es": "0.356.0",
1055
1055
  "jsqr": "^1.4.0"
1056
1056
  }
1057
1057
  }