@cosmicdrift/kumiko-bundled-features 0.337.1 → 0.339.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 (235) hide show
  1. package/dist/agent-tools/tool-catalog.js +37 -17
  2. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +3 -2
  3. package/dist/auth-email-password/handlers/invite-accept.write.js +3 -4
  4. package/dist/auth-email-password/handlers/invite-info.query.js +4 -1
  5. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +3 -4
  6. package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -1
  7. package/dist/auth-email-password/handlers/signup-request.write.js +2 -2
  8. package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
  9. package/dist/auth-email-password/web/auth-gate.js +25 -1
  10. package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
  11. package/dist/auth-email-password/web/auth-redirect.js +58 -0
  12. package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
  13. package/dist/auth-email-password/web/client-plugin.js +2 -0
  14. package/dist/auth-email-password/web/index.d.ts +2 -1
  15. package/dist/auth-email-password/web/index.js +1 -0
  16. package/dist/auth-email-password/web/session.d.ts +3 -1
  17. package/dist/auth-email-password/web/session.js +8 -2
  18. package/dist/auth-mfa/feature.js +1 -1
  19. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
  20. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +2 -1
  21. package/dist/auth-mfa/handlers/verify.write.js +1 -1
  22. package/dist/auth-mfa/mfa-code-verifier.js +1 -1
  23. package/dist/auth-mfa/mfa-status-checker.js +1 -1
  24. package/dist/billing-foundation/aggregate-id.d.ts +2 -0
  25. package/dist/billing-foundation/aggregate-id.js +8 -0
  26. package/dist/billing-foundation/checkout-core.d.ts +3 -0
  27. package/dist/billing-foundation/checkout-core.js +3 -0
  28. package/dist/billing-foundation/constants.d.ts +38 -0
  29. package/dist/billing-foundation/constants.js +43 -0
  30. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
  31. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
  32. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
  33. package/dist/billing-foundation/consumer-protection/consent-gate.js +96 -0
  34. package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
  35. package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
  36. package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
  37. package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
  38. package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
  39. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
  40. package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
  41. package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
  42. package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
  43. package/dist/billing-foundation/consumer-protection/register-termination.js +40 -0
  44. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +45 -0
  45. package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
  46. package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
  47. package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
  48. package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -0
  49. package/dist/billing-foundation/consumer-protection/termination-pages.js +268 -0
  50. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +73 -0
  51. package/dist/billing-foundation/consumer-protection/termination-record.js +230 -0
  52. package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
  53. package/dist/billing-foundation/consumer-protection/termination-request.js +231 -0
  54. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
  55. package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
  56. package/dist/billing-foundation/events.d.ts +105 -2
  57. package/dist/billing-foundation/events.js +88 -2
  58. package/dist/billing-foundation/feature.js +6 -2
  59. package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
  60. package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
  61. package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
  62. package/dist/billing-foundation/handlers/process-event.write.js +43 -15
  63. package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
  64. package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
  65. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +17 -1
  66. package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
  67. package/dist/billing-foundation/i18n.js +61 -0
  68. package/dist/billing-foundation/index.d.ts +3 -2
  69. package/dist/billing-foundation/index.js +2 -1
  70. package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
  71. package/dist/billing-foundation/types.d.ts +54 -9
  72. package/dist/billing-foundation/validate-options.js +51 -10
  73. package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
  74. package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
  75. package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
  76. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
  77. package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
  78. package/dist/billing-foundation/webhook-handler.js +5 -2
  79. package/dist/cap-counter/with-cap-enforcement.js +7 -3
  80. package/dist/cap-overview/access.d.ts +2 -1
  81. package/dist/cap-overview/access.js +26 -5
  82. package/dist/cap-overview/feature.d.ts +4 -0
  83. package/dist/cap-overview/feature.js +8 -6
  84. package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
  85. package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
  86. package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
  87. package/dist/cap-overview/index.d.ts +2 -2
  88. package/dist/cap-overview/index.js +2 -2
  89. package/dist/cap-overview/screens.d.ts +1 -1
  90. package/dist/cap-overview/screens.js +17 -16
  91. package/dist/channel-discord/discord-channel.d.ts +6 -0
  92. package/dist/channel-discord/discord-channel.js +19 -0
  93. package/dist/channel-discord/feature.d.ts +3 -0
  94. package/dist/channel-discord/feature.js +30 -0
  95. package/dist/channel-discord/index.d.ts +2 -0
  96. package/dist/channel-discord/index.js +2 -0
  97. package/dist/channel-slack/feature.d.ts +3 -0
  98. package/dist/channel-slack/feature.js +30 -0
  99. package/dist/channel-slack/index.d.ts +2 -0
  100. package/dist/channel-slack/index.js +2 -0
  101. package/dist/channel-slack/slack-channel.d.ts +5 -0
  102. package/dist/channel-slack/slack-channel.js +16 -0
  103. package/dist/channel-teams/feature.d.ts +3 -0
  104. package/dist/channel-teams/feature.js +34 -0
  105. package/dist/channel-teams/index.d.ts +2 -0
  106. package/dist/channel-teams/index.js +2 -0
  107. package/dist/channel-teams/teams-channel.d.ts +5 -0
  108. package/dist/channel-teams/teams-channel.js +38 -0
  109. package/dist/channel-telegram/feature.d.ts +3 -0
  110. package/dist/channel-telegram/feature.js +28 -0
  111. package/dist/channel-telegram/index.d.ts +2 -0
  112. package/dist/channel-telegram/index.js +2 -0
  113. package/dist/channel-telegram/telegram-channel.d.ts +8 -0
  114. package/dist/channel-telegram/telegram-channel.js +54 -0
  115. package/dist/config/handlers/set.write.js +18 -2
  116. package/dist/crypto-shredding/feature.js +3 -2
  117. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
  118. package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -19
  119. package/dist/custom-fields/feature.js +9 -6
  120. package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -1
  121. package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -1
  122. package/dist/custom-fields/handlers/set-custom-field.write.js +2 -1
  123. package/dist/custom-fields/wire-user-data-rights.js +1 -1
  124. package/dist/data-retention/feature.js +1 -1
  125. package/dist/delivery/channel-context.d.ts +2 -1
  126. package/dist/delivery/channel-context.js +2 -2
  127. package/dist/delivery/chat-webhook-channel.d.ts +19 -0
  128. package/dist/delivery/chat-webhook-channel.js +48 -0
  129. package/dist/delivery/chat-webhook-sender.d.ts +21 -0
  130. package/dist/delivery/chat-webhook-sender.js +102 -0
  131. package/dist/delivery/delivery-service.js +12 -7
  132. package/dist/delivery/index.d.ts +2 -0
  133. package/dist/delivery/index.js +2 -0
  134. package/dist/delivery/jobs.js +1 -1
  135. package/dist/delivery/testing.d.ts +2 -0
  136. package/dist/delivery/types.d.ts +3 -1
  137. package/dist/delivery/types.js +1 -2
  138. package/dist/document-ingest-foundation/feature.js +3 -2
  139. package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
  140. package/dist/files-tenant-data/index.js +1 -1
  141. package/dist/form-draft/feature.js +1 -1
  142. package/dist/form-draft/handlers/discard.write.js +2 -1
  143. package/dist/form-draft/handlers/save.write.js +2 -1
  144. package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
  145. package/dist/inbound-mail-foundation/connect-routes.js +13 -8
  146. package/dist/inbound-mail-foundation/feature.js +1 -1
  147. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +2 -1
  148. package/dist/inbound-mail-foundation/index.d.ts +1 -0
  149. package/dist/inbound-mail-foundation/index.js +1 -0
  150. package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
  151. package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
  152. package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
  153. package/dist/inbound-mail-foundation/types.d.ts +18 -1
  154. package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
  155. package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
  156. package/dist/managed-pages/handlers/set.write.js +2 -1
  157. package/dist/notes-history/executor.d.ts +2 -0
  158. package/dist/notes-history/executor.js +9 -1
  159. package/dist/notes-history/feature.d.ts +2 -3
  160. package/dist/notes-history/feature.js +2 -1
  161. package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
  162. package/dist/notes-history/handlers/add-note.write.js +21 -4
  163. package/dist/notes-history-user-data/hooks.js +26 -8
  164. package/dist/renderer-simple/feature.d.ts +8 -2
  165. package/dist/renderer-simple/feature.js +8 -5
  166. package/dist/renderer-simple/index.d.ts +2 -2
  167. package/dist/renderer-simple/index.js +1 -1
  168. package/dist/renderer-simple/simple-renderer.d.ts +15 -0
  169. package/dist/renderer-simple/simple-renderer.js +74 -27
  170. package/dist/secrets/feature.js +1 -1
  171. package/dist/sessions/feature.js +1 -1
  172. package/dist/sessions/handlers/revoke-all-for-user.write.js +2 -1
  173. package/dist/subscription-mollie/plugin-methods.js +1 -0
  174. package/dist/subscription-mollie/verify-webhook.js +5 -3
  175. package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
  176. package/dist/subscription-stripe/plugin-methods.js +78 -13
  177. package/dist/subscription-stripe/verify-webhook.js +7 -3
  178. package/dist/tags/executor.d.ts +2 -0
  179. package/dist/tags/executor.js +9 -1
  180. package/dist/tags/feature.d.ts +4 -3
  181. package/dist/tags/feature.js +4 -2
  182. package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
  183. package/dist/tags/handlers/assign-tag.write.js +4 -4
  184. package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
  185. package/dist/tags/handlers/remove-tag.write.js +3 -3
  186. package/dist/tenant-handover/handlers/claim.write.js +2 -2
  187. package/dist/tenant-lifecycle/feature.js +1 -1
  188. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +4 -1
  189. package/dist/tenant-lifecycle/handlers/request-destruction.write.js +2 -1
  190. package/dist/tier-engine/feature.js +1 -1
  191. package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -1
  192. package/dist/tier-engine/handlers/set-tenant-tier.write.js +2 -1
  193. package/dist/user/handlers/detail.query.js +1 -0
  194. package/dist/user/handlers/list.query.js +1 -0
  195. package/dist/user-data-rights/feature.js +2 -2
  196. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  197. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
  198. package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
  199. package/dist/user-data-rights/handlers/download-by-job.query.js +2 -1
  200. package/dist/user-data-rights/handlers/download-by-token.query.js +2 -1
  201. package/dist/user-data-rights/handlers/export-status.query.js +2 -1
  202. package/dist/user-data-rights/handlers/lift-restriction.write.js +3 -4
  203. package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
  204. package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +2 -1
  205. package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
  206. package/dist/user-data-rights/handlers/request-export.write.js +2 -1
  207. package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
  208. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +2 -1
  209. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  210. package/dist/user-data-rights/run-forget-cleanup.js +30 -15
  211. package/dist/workflow-runner/event-subscriber.js +6 -2
  212. package/dist/workflow-runner/event-trigger.js +5 -0
  213. package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
  214. package/dist/workflow-runner/pending-projection.js +1 -0
  215. package/dist/workflow-runner/runner.d.ts +7 -1
  216. package/dist/workflow-runner/runner.js +1 -1
  217. package/dist/workflow-runner/tables.js +8 -1
  218. package/package.json +41 -9
  219. package/src/agent-tools/changes.json +14 -0
  220. package/src/auth-email-password/changes.json +7 -0
  221. package/src/billing-foundation/changes.json +83 -0
  222. package/src/cap-counter/changes.json +7 -0
  223. package/src/cap-overview/changes.json +14 -0
  224. package/src/config/changes.json +7 -0
  225. package/src/crypto-shredding/changes.json +13 -0
  226. package/src/custom-fields/changes.json +7 -0
  227. package/src/delivery/changes.json +14 -0
  228. package/src/inbound-mail-foundation/changes.json +7 -0
  229. package/src/notes-history/changes.json +7 -0
  230. package/src/renderer-simple/changes.json +9 -1
  231. package/src/subscription-mollie/changes.json +13 -0
  232. package/src/subscription-stripe/changes.json +13 -0
  233. package/src/tags/changes.json +7 -0
  234. package/src/user-data-rights/changes.json +6 -0
  235. package/src/workflow-runner/changes.json +7 -0
@@ -28,9 +28,10 @@ export type TagsFeatureOptions = {
28
28
  * `tag-assignment`; the `tag` catalog stays tenant-wide by design.
29
29
  *
30
30
  * `ownership.write` is separate and does NOT affect list/read. It's
31
- * consulted by the framework's generic delete/forget/restore paths (not by
32
- * this feature's own assign-tag handler — see createTagsFeature's
33
- * boot-guard comment). A `from()` rule there also gates GDPR erasure
31
+ * consulted on every assignment write: assign-tag (create/restore),
32
+ * remove-tag (delete) and the framework's generic forget path. The
33
+ * delete-tag cascade stays ungated, since deleting a catalog tag detaches
34
+ * it everywhere. A `from()` rule there also gates GDPR erasure
34
35
  * (`forget`): if the rule's role map doesn't cover whatever role the
35
36
  * erasure/retention pipeline runs as, `forget` denies instead of
36
37
  * crypto-shredding — a silent Art.17 failure, not a thrown error. Make
@@ -21,6 +21,7 @@ import { defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateH
21
21
  import { hasWhereRule } from "../shared/index.js";
22
22
  import { DEFAULT_TAG_ACCESS, TAGS_FEATURE_NAME } from "./constants.js";
23
23
  import { createTagAssignmentEntity, tagEntity } from "./entity.js";
24
+ import { createTagAssignmentExecutor } from "./executor.js";
24
25
  import { createAssignTagHandler } from "./handlers/assign-tag.write.js";
25
26
  import { createCreateTagHandler } from "./handlers/create-tag.write.js";
26
27
  import { createDeleteTagHandler } from "./handlers/delete-tag.write.js";
@@ -45,8 +46,9 @@ function registerTags(r, access, toggleable, ownership, parents) {
45
46
  r.writeHandler(createCreateTagHandler(access));
46
47
  r.writeHandler(createUpdateTagHandler(access));
47
48
  r.writeHandler(createDeleteTagHandler(access));
48
- r.writeHandler(createAssignTagHandler(access));
49
- r.writeHandler(createRemoveTagHandler(access));
49
+ const assignmentExecutor = createTagAssignmentExecutor(ownership);
50
+ r.writeHandler(createAssignTagHandler(access, assignmentExecutor));
51
+ r.writeHandler(createRemoveTagHandler(access, assignmentExecutor));
50
52
  // Convention aliases for entityList/entityEdit — create stays flat-payload
51
53
  // (payloadMode=values); update must accept the {id,version,changes} envelope
52
54
  // entityEdit sends (legacy update-tag stays for TagManager). delete stays
@@ -1,3 +1,4 @@
1
1
  import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
- export declare function createAssignTagHandler(access?: AccessRule): WriteHandlerDef;
2
+ import { tagAssignmentExecutor } from "../executor.js";
3
+ export declare function createAssignTagHandler(access?: AccessRule, assignmentExecutor?: typeof tagAssignmentExecutor): WriteHandlerDef;
3
4
  export declare const assignTagHandler: WriteHandlerDef;
@@ -25,7 +25,7 @@ import { assignTagPayloadSchema } from "../schemas.js";
25
25
  // the aggregate-id is tenant-scoped, so this is not a cross-tenant hole, but
26
26
  // without the check any dispatch-eligible tenant user could tag an object they
27
27
  // are not allowed to read. See shared/parent-visibility.ts.
28
- export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
28
+ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS, assignmentExecutor = tagAssignmentExecutor) {
29
29
  return {
30
30
  name: "assign-tag",
31
31
  schema: assignTagPayloadSchema,
@@ -37,11 +37,11 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
37
37
  if (denied)
38
38
  return denied;
39
39
  const id = tagAssignmentAggregateId(event.user.tenantId, payload.tagId, payload.entityType, payload.entityId);
40
- const existing = await tagAssignmentExecutor.detail({ id }, event.user, ctx.db);
40
+ const existing = await assignmentExecutor.detail({ id }, event.user, ctx.db);
41
41
  if (existing) {
42
42
  return { isSuccess: true, data: { id } };
43
43
  }
44
- const restored = await tagAssignmentExecutor.restore({ id }, event.user, ctx.db);
44
+ const restored = await assignmentExecutor.restore({ id }, event.user, ctx.db);
45
45
  if (restored.isSuccess)
46
46
  return { isSuccess: true, data: { id } };
47
47
  // A concurrent first-time assign can also land here: the other caller's
@@ -57,7 +57,7 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS) {
57
57
  const tag = await tagExecutor.detail({ id: payload.tagId }, event.user, ctx.db);
58
58
  if (!tag)
59
59
  return writeFailure(new NotFoundError("tag", payload.tagId));
60
- const created = await tagAssignmentExecutor.create({
60
+ const created = await assignmentExecutor.create({
61
61
  id,
62
62
  tagId: payload.tagId,
63
63
  entityType: payload.entityType,
@@ -1,3 +1,4 @@
1
1
  import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
- export declare function createRemoveTagHandler(access?: AccessRule): WriteHandlerDef;
2
+ import { tagAssignmentExecutor } from "../executor.js";
3
+ export declare function createRemoveTagHandler(access?: AccessRule, assignmentExecutor?: typeof tagAssignmentExecutor): WriteHandlerDef;
3
4
  export declare const removeTagHandler: WriteHandlerDef;
@@ -10,7 +10,7 @@ import { removeTagPayloadSchema } from "../schemas.js";
10
10
  // Host reference: entityType/entityId are client input, so the caller must be
11
11
  // able to see the host row before detaching anything from it — see
12
12
  // shared/parent-visibility.ts.
13
- export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS) {
13
+ export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS, assignmentExecutor = tagAssignmentExecutor) {
14
14
  return {
15
15
  name: "remove-tag",
16
16
  schema: removeTagPayloadSchema,
@@ -22,11 +22,11 @@ export function createRemoveTagHandler(access = DEFAULT_TAG_ACCESS) {
22
22
  if (denied)
23
23
  return denied;
24
24
  const id = tagAssignmentAggregateId(event.user.tenantId, payload.tagId, payload.entityType, payload.entityId);
25
- const existing = await tagAssignmentExecutor.detail({ id }, event.user, ctx.db);
25
+ const existing = await assignmentExecutor.detail({ id }, event.user, ctx.db);
26
26
  if (!existing) {
27
27
  return { isSuccess: true, data: { id } };
28
28
  }
29
- return tagAssignmentExecutor.delete({ id }, event.user, ctx.db);
29
+ return assignmentExecutor.delete({ id }, event.user, ctx.db);
30
30
  },
31
31
  };
32
32
  }
@@ -28,6 +28,7 @@ export function createClaimTenantHandoverHandler(opts = {}) {
28
28
  schema: z.object({ token: z.string().min(1), entityType: z.string().min(1) }),
29
29
  access: { roles: ["Member", "User", "TenantAdmin", "SystemAdmin"] },
30
30
  escapeHatch: {
31
+ grants: ["unsafeRaw"],
31
32
  reason: "the ownership change moves rows OUT of a source tenant this caller has no membership " +
32
33
  "in — the framework's declared cross-tenant operation for try-before-signup, not a " +
33
34
  "per-consumer acknowledgeCrossTenant workaround",
@@ -35,8 +36,7 @@ export function createClaimTenantHandoverHandler(opts = {}) {
35
36
  agent: { expose: false },
36
37
  rateLimit: { per: "user", limit: 10, windowSeconds: 60 },
37
38
  handler: async (event, ctx) => {
38
- const db = ctx.db.unsafeRaw("tenant-handover claim: cross-tenant read + ownership write, legitimized by the " +
39
- "row-bound grant redeemed below");
39
+ const db = ctx.db.unsafeRaw();
40
40
  const rootLocation = resolveRootAnchorLocation(ctx.registry, db, event.payload.entityType);
41
41
  if (!rootLocation)
42
42
  return notTransferable(event.payload.entityType);
@@ -74,7 +74,7 @@ export function createTenantLifecycleFeature() {
74
74
  }
75
75
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
76
76
  await runTenantDestructionSweep({
77
- db: ctx.db.unsafeRaw(RUN_TENANT_DESTRUCTION_REASON), // @cast-boundary db-operator — jobs never run inside a DbTx
77
+ db: ctx.db.unsafeRaw(), // @cast-boundary db-operator — jobs never run inside a DbTx
78
78
  registry: ctx.registry,
79
79
  now: T.Now.instant(),
80
80
  log: (message) => ctx.log?.warn(message),
@@ -15,11 +15,14 @@ export const cancelDestructionWrite = defineWriteHandler({
15
15
  access: { roles: ["TenantOwner", "Admin"] },
16
16
  description: "Returns a tenant whose destruction was requested back to active and clears the grace period; use it to undo a close-account request while the grace period is still running.",
17
17
  escapeHatch: {
18
+ grants: ["unsafeRaw"],
18
19
  reason: CANCEL_DESTRUCTION_TENANT_ROW_REASON,
19
20
  },
20
21
  handler: async (event, ctx) => {
21
22
  const tenantId = event.user.tenantId;
22
- const row = await fetchOne(ctx.db.unsafeRaw(CANCEL_DESTRUCTION_TENANT_ROW_REASON), tenantTable, { id: tenantId });
23
+ const row = await fetchOne(ctx.db.unsafeRaw(), tenantTable, {
24
+ id: tenantId,
25
+ });
23
26
  if (!row) {
24
27
  return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
25
28
  }
@@ -19,11 +19,12 @@ export const requestDestructionWrite = defineWriteHandler({
19
19
  description: "Puts the caller's own tenant into destroyRequested, starts the compliance-profile grace period after which its data is erased, and revokes every session in the tenant; use it when an account owner asks to close their account.",
20
20
  agent: { risk: "high" },
21
21
  escapeHatch: {
22
+ grants: ["unsafeRaw"],
22
23
  reason: REQUEST_DESTRUCTION_TENANT_ROW_REASON,
23
24
  },
24
25
  handler: async (event, ctx) => {
25
26
  const tenantId = event.user.tenantId;
26
- const runner = ctx.db.unsafeRaw(REQUEST_DESTRUCTION_TENANT_ROW_REASON);
27
+ const runner = ctx.db.unsafeRaw();
27
28
  const row = await fetchOne(runner, tenantTable, { id: tenantId });
28
29
  if (!row) {
29
30
  return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
@@ -303,7 +303,7 @@ export function createTierEngineFeature(opts = {}) {
303
303
  const rawDb = ctx.systemDb
304
304
  ? ctx.systemDb.unsafeRaw(autoDefaultTierHookReason)
305
305
  : ctx.db && "unsafeRaw" in ctx.db
306
- ? ctx.db.unsafeRaw(autoDefaultTierHookReason)
306
+ ? ctx.db.unsafeRaw()
307
307
  : undefined;
308
308
  // skip: defensive — inTransaction phase always sets db, but AppContext's
309
309
  // type makes it optional. Throwing would be overreach (lifecycle
@@ -15,11 +15,12 @@ export const getTenantTierQuery = defineQueryHandler({
15
15
  schema: z.object({ tenantId: z.string().min(1) }),
16
16
  access: { roles: ["SystemAdmin"] },
17
17
  escapeHatch: {
18
+ grants: ["unsafeRaw"],
18
19
  reason: GET_TENANT_TIER_REASON,
19
20
  },
20
21
  handler: async (query, ctx) => {
21
22
  const tenantId = query.payload.tenantId; // @cast-boundary engine-bridge
22
- const tdb = createTenantDb(ctx.db.unsafeRaw(GET_TENANT_TIER_REASON), tenantId, "system");
23
+ const tdb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
23
24
  const row = await fetchOne(tdb, tierAssignmentTable, { tenantId });
24
25
  return row ?? null;
25
26
  },
@@ -46,11 +46,12 @@ export function createSetTenantTierWrite(opts = {}) {
46
46
  }),
47
47
  access: { roles: ["SystemAdmin"] },
48
48
  escapeHatch: {
49
+ grants: ["unsafeRaw"],
49
50
  reason: SET_TENANT_TIER_REASON,
50
51
  },
51
52
  handler: async (event, ctx) => {
52
53
  const tenantId = event.payload.tenantId; // @cast-boundary engine-bridge
53
- const rawDb = ctx.db.unsafeRaw(SET_TENANT_TIER_REASON);
54
+ const rawDb = ctx.db.unsafeRaw();
54
55
  const tdb = createTenantDb(rawDb, tenantId, "system");
55
56
  const systemUser = { ...event.user, tenantId };
56
57
  const tier = event.payload.tier;
@@ -6,6 +6,7 @@ import { attachTenantLabels, dbForList } from "./list.query.js";
6
6
  // bound to their own tenant's memberships (served by the tenant feature).
7
7
  const baseDetail = defineEntityDetailHandler("user", userEntity, {
8
8
  access: { roles: access.systemAdmin },
9
+ agent: { risk: "high" },
9
10
  description: "Loads one identity record by id for a SystemAdmin, enriched with the same derived `tenants` membership label as the user list; tenant-level admins do not reach it.",
10
11
  });
11
12
  // Same tenants enrichment as user:list — derived field placeholder is "".
@@ -6,6 +6,7 @@ import { tenantMembershipsTable, tenantTable } from "../../tenant/index.js";
6
6
  import { userEntity } from "../schema/user.js";
7
7
  const baseList = defineEntityListHandler("user", userEntity, {
8
8
  access: { roles: access.systemAdmin },
9
+ agent: { risk: "high" },
9
10
  description: "Lists identity records across every tenant for a SystemAdmin roster, each row carrying a derived `tenants` label naming the tenants the user belongs to and their roles there.",
10
11
  });
11
12
  async function loadMemberships(db, userIds) {
@@ -411,7 +411,7 @@ export function createUserDataRightsFeature(opts = {}) {
411
411
  // Discriminator wird via handlerName="user-data-rights:run-export-
412
412
  // jobs" im Secret-Read-Audit erfasst.
413
413
  const exportUserId = ctx._userId ?? SYSTEM_USER_ID;
414
- const exportDb = ctx.db.unsafeRaw(RUN_EXPORT_JOBS_REASON); // @cast-boundary db-operator — jobs never run inside a DbTx
414
+ const exportDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
415
415
  const exportRegistry = ctx.registry;
416
416
  // JobContext carries ctx.meter (raw), not ctx.metrics — that bound
417
417
  // handle only exists on HandlerContext (built per write/query call
@@ -496,7 +496,7 @@ export function createUserDataRightsFeature(opts = {}) {
496
496
  }
497
497
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
498
498
  const forgetUserId = ctx._userId ?? SYSTEM_USER_ID;
499
- const forgetDb = ctx.db.unsafeRaw(RUN_FORGET_CLEANUP_REASON); // @cast-boundary db-operator — jobs never run inside a DbTx
499
+ const forgetDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
500
500
  const forgetRegistry = ctx.registry;
501
501
  const tenantModel = await resolveAppTenantModel({
502
502
  registry: forgetRegistry,
@@ -27,6 +27,7 @@ export const cancelDeletionWrite = defineWriteHandler({
27
27
  },
28
28
  description: "Withdraws the calling user's own pending account-deletion request and puts the account back to active, accepted only while the grace period is still running.",
29
29
  escapeHatch: {
30
+ grants: ["unsafeRaw", "globalWrites"],
30
31
  reason: APPEND_LIFECYCLE_EVENT_REASON,
31
32
  },
32
33
  handler: async (event, ctx) => {
@@ -44,7 +45,7 @@ export const cancelDeletionWrite = defineWriteHandler({
44
45
  if (!isWithinGracePeriod(row.gracePeriodEnd)) {
45
46
  return writeFailure(new UnprocessableError("grace_period_expired"));
46
47
  }
47
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), event.user.id, {
48
+ await updateUserLifecycle(ctx.db.unsafeRaw(), event.user.id, {
48
49
  status: USER_STATUS.Active,
49
50
  gracePeriodEnd: null,
50
51
  // #354/1: closes the replay-after-cancel window — a still-TTL-valid email
@@ -68,7 +68,7 @@ export function createConfirmDeletionByTokenHandler(opts = {}) {
68
68
  secret: opts.deletionTokenSecret,
69
69
  loadPendingRequestId: (userId) => readPendingDeletionRequestId(ctx, userId),
70
70
  commitDeletion: async (userId, requestId) => {
71
- const res = await startDeletionGracePeriod(ctx, userId, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"), { pendingDeletionRequestId: requestId });
71
+ const res = await startDeletionGracePeriod(ctx, userId, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw(), { pendingDeletionRequestId: requestId });
72
72
  if (!res.ok)
73
73
  return false;
74
74
  gracePeriodEndIso = res.gracePeriodEnd.toString();
@@ -5,6 +5,7 @@ import { downloadAttemptEntity } from "../schema/download-attempt.js";
5
5
  // non-convention QN; entityList needs the convention `download-attempt:list`.
6
6
  export const downloadAttemptListQuery = defineEntityListHandler("download-attempt", downloadAttemptEntity, {
7
7
  access: { roles: access.systemAdmin },
8
+ agent: { risk: "high" },
8
9
  escapeHatch: {
9
10
  reason: "SystemAdmin DPO triage of invalid export-download attempts needs every tenant's rows to spot brute-force patterns across tenants",
10
11
  },
@@ -47,6 +47,7 @@ export const downloadByJobQuery = defineQueryHandler({
47
47
  }, // openToAll = auth-required, kein anonymous
48
48
  description: "Returns a short-lived signed download URL for the calling user's own finished data-export job named by job id, backing the download button in the privacy center once export-status reports the job done.",
49
49
  escapeHatch: {
50
+ grants: ["unsafeRaw"],
50
51
  reason: DOWNLOAD_BY_JOB_REASON,
51
52
  },
52
53
  handler: async (query, ctx) => {
@@ -60,7 +61,7 @@ export const downloadByJobQuery = defineQueryHandler({
60
61
  // Wert (603/2).
61
62
  const auditIp = requestContext.get()?.ip ?? null;
62
63
  const auditUa = requestContext.get()?.userAgent ?? null;
63
- const runner = ctx.db.unsafeRaw(DOWNLOAD_BY_JOB_REASON);
64
+ const runner = ctx.db.unsafeRaw();
64
65
  // Step 1-2: job-lookup + cross-user-isolation
65
66
  const jobRow = await fetchOne(runner, exportJobsTable, { id: jobId });
66
67
  if (!jobRow || jobRow.userId !== userId) {
@@ -58,12 +58,13 @@ export const downloadByTokenQuery = defineQueryHandler({
58
58
  // Memory `feedback_security_default_on`.
59
59
  rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
60
60
  escapeHatch: {
61
+ grants: ["unsafeRaw"],
61
62
  reason: DOWNLOAD_BY_TOKEN_REASON,
62
63
  },
63
64
  handler: async (query, ctx) => {
64
65
  const T = getTemporal();
65
66
  const now = T.Now.instant();
66
- const runner = ctx.db.unsafeRaw(DOWNLOAD_BY_TOKEN_REASON);
67
+ const runner = ctx.db.unsafeRaw();
67
68
  // Step 1: hash + lookup
68
69
  const hash = await hashDownloadToken(query.payload.token);
69
70
  const tokenRow = await fetchOne(runner, exportDownloadTokensTable, {
@@ -26,10 +26,11 @@ export const exportStatusQuery = defineQueryHandler({
26
26
  },
27
27
  description: "Returns the calling user's own most recent data-export job with its status, expiry and error, or hasJob false, for polling after a request-export while the job is still running.",
28
28
  escapeHatch: {
29
+ grants: ["unsafeRaw"],
29
30
  reason: EXPORT_STATUS_REASON,
30
31
  },
31
32
  handler: async (query, ctx) => {
32
- const rows = await selectMany(ctx.db.unsafeRaw(EXPORT_STATUS_REASON), exportJobsTable, { userId: query.user.id }, { limit: 1, orderBy: { col: "requestedAt", direction: "desc" } });
33
+ const rows = await selectMany(ctx.db.unsafeRaw(), exportJobsTable, { userId: query.user.id }, { limit: 1, orderBy: { col: "requestedAt", direction: "desc" } });
33
34
  const latest = rows[0];
34
35
  if (!latest)
35
36
  return { hasJob: false };
@@ -5,8 +5,6 @@ import { USER_STATUS, userTable } from "../../user/index.js";
5
5
  import { denyIfTargetOutsideAdminTenant } from "../lib/deny-if-target-outside-admin-tenant.js";
6
6
  import { updateUserLifecycle } from "../lib/update-user-lifecycle.js";
7
7
  const LIFT_RESTRICTION_ESCAPE_HATCH_REASON = "checks the target user's membership in the admin's tenant and appends the user lifecycle status change on the SYSTEM_TENANT_ID user stream, both via DbRunner helpers outside the admin's own tenant scope.";
8
- const CHECK_TARGET_MEMBERSHIP_REASON = "checks the target user's membership in the admin's tenant via the DbRunner helper";
9
- const APPEND_LIFECYCLE_EVENT_REASON = "appends the user lifecycle event on the SYSTEM_TENANT_ID user stream";
10
8
  // POST /api/user/lift-restriction (S2.U6) — DSGVO Art. 18 Reverse.
11
9
  //
12
10
  // Operator-only. A Restricted user's own session is unconditionally
@@ -33,11 +31,12 @@ export const liftRestrictionWrite = defineWriteHandler({
33
31
  access: { roles: access.admin },
34
32
  description: "Lifts a GDPR Art. 18 processing restriction on the named user and returns the account to active; operator-only, because a restricted user's own session is rejected and cannot reach this endpoint.",
35
33
  escapeHatch: {
34
+ grants: ["unsafeRaw", "globalWrites"],
36
35
  reason: LIFT_RESTRICTION_ESCAPE_HATCH_REASON,
37
36
  },
38
37
  handler: async (event, ctx) => {
39
38
  const targetUserId = event.payload.userId;
40
- const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(CHECK_TARGET_MEMBERSHIP_REASON), event.user, targetUserId);
39
+ const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(), event.user, targetUserId);
41
40
  if (outside)
42
41
  return outside;
43
42
  const userRow = await ctx.db.global(userTable).fetchOne({
@@ -54,7 +53,7 @@ export const liftRestrictionWrite = defineWriteHandler({
54
53
  details: { currentStatus },
55
54
  }));
56
55
  }
57
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), targetUserId, {
56
+ await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, {
58
57
  status: USER_STATUS.Active,
59
58
  });
60
59
  return {
@@ -33,6 +33,7 @@ export const myAuditLogQuery = defineQueryHandler({
33
33
  },
34
34
  description: "Returns the calling user's own event-store entries across all their tenant memberships, paged and filterable by aggregate type, event type and time range, for the GDPR Art. 15 self-disclosure; it can never read another user's history.",
35
35
  escapeHatch: {
36
+ grants: ["unsafeRaw"],
36
37
  reason: MY_AUDIT_LOG_REASON,
37
38
  },
38
39
  handler: async (query, ctx) => {
@@ -52,7 +53,7 @@ export const myAuditLogQuery = defineQueryHandler({
52
53
  }
53
54
  if (p.before)
54
55
  where["id"] = { lt: BigInt(p.before) };
55
- const rows = await selectMany(ctx.db.unsafeRaw(MY_AUDIT_LOG_REASON), eventsTable, where, {
56
+ const rows = await selectMany(ctx.db.unsafeRaw(), eventsTable, where, {
56
57
  orderBy: { col: "id", direction: "desc" },
57
58
  limit: p.limit,
58
59
  });
@@ -36,6 +36,7 @@ export function createRequestDeletionByEmailHandler(opts = {}) {
36
36
  // Defense-in-depth gegen Email-Probing auf dem anonymen Endpoint.
37
37
  rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
38
38
  escapeHatch: {
39
+ grants: ["unsafeRaw", "globalWrites"],
39
40
  reason: APPEND_LIFECYCLE_EVENT_REASON,
40
41
  },
41
42
  handler: async (event, ctx) => {
@@ -56,7 +57,7 @@ export function createRequestDeletionByEmailHandler(opts = {}) {
56
57
  // user-Row landet und in die Token-HMAC-Purpose gefaltet wird. cancel
57
58
  // nullt sie → ein nach Cancel nachgespieltes Token verifiziert nicht mehr.
58
59
  const requestId = crypto.randomUUID();
59
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), userRow["id"], {
60
+ await updateUserLifecycle(ctx.db.unsafeRaw(), userRow["id"], {
60
61
  pendingDeletionRequestId: requestId,
61
62
  });
62
63
  const { token, expiresAt } = signDeletionToken(userRow["id"], requestId, DELETION_VERIFY_TTL_MINUTES, opts.deletionTokenSecret);
@@ -29,7 +29,7 @@ export function createRequestDeletionHandler(opts = {}) {
29
29
  description: "Starts the GDPR Art. 17 deletion of the calling user's own account by arming the grace period from the tenant compliance profile and mailing a confirmation, after which only cancel-deletion can stop the erasure.",
30
30
  agent: { risk: "high" },
31
31
  handler: async (event, ctx) => {
32
- const res = await startDeletionGracePeriod(ctx, event.user.id, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"));
32
+ const res = await startDeletionGracePeriod(ctx, event.user.id, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw());
33
33
  if (!res.ok)
34
34
  return writeFailure(res.error);
35
35
  const { gracePeriodEnd, userEmail, userLocale } = res;
@@ -51,13 +51,14 @@ export const requestExportWrite = defineWriteHandler({
51
51
  },
52
52
  description: "Queues a GDPR Art. 15 and 20 data export for the calling user and returns its job id, handing back the running job with isExisting true instead of a second one when an export is already pending.",
53
53
  escapeHatch: {
54
+ grants: ["unsafeRaw"],
54
55
  reason: REQUEST_EXPORT_REASON,
55
56
  },
56
57
  handler: async (event, ctx) => {
57
58
  const userId = event.user.id;
58
59
  const T = getTemporal();
59
60
  const now = T.Now.instant();
60
- const exportJobRunner = ctx.db.unsafeRaw(REQUEST_EXPORT_REASON);
61
+ const exportJobRunner = ctx.db.unsafeRaw();
61
62
  // Pre-Check: exportJobRunner bypasses the TenantDb-Filter — the
62
63
  // TenantDb-Wrapper would hide cross-tenant jobs.
63
64
  const existing = await findActiveJob(exportJobRunner, userId);
@@ -59,7 +59,7 @@ export const restrictAccountWrite = defineWriteHandler({
59
59
  details: { reason: "admin_required_for_other_user" },
60
60
  }));
61
61
  }
62
- const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw("checks the target user's membership in the admin's tenant via the DbRunner helper"), event.user, targetUserId);
62
+ const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(), event.user, targetUserId);
63
63
  if (outside)
64
64
  return outside;
65
65
  }
@@ -82,7 +82,7 @@ export const restrictAccountWrite = defineWriteHandler({
82
82
  details: { currentStatus },
83
83
  }));
84
84
  }
85
- await updateUserLifecycle(ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"), targetUserId, { status: USER_STATUS.Restricted });
85
+ await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, { status: USER_STATUS.Restricted });
86
86
  // Cross-Feature: alle live sessions revoken — sonst koennte der User
87
87
  // mit existierendem JWT bis zur Token-Expiry weiter schreiben.
88
88
  // ctx.writeAs(systemUser, ...) damit der privileged-Handler die
@@ -26,6 +26,7 @@ export function createRunForgetCleanupHandler(opts = {}) {
26
26
  // records without softDelete — an agent must never be able to trigger it.
27
27
  agent: { expose: false, risk: "high" },
28
28
  escapeHatch: {
29
+ grants: ["unsafeRaw"],
29
30
  reason: RUN_FORGET_CLEANUP_REASON,
30
31
  },
31
32
  handler: async (_event, ctx) => {
@@ -35,7 +36,7 @@ export function createRunForgetCleanupHandler(opts = {}) {
35
36
  }));
36
37
  }
37
38
  const T = getTemporal();
38
- const runner = ctx.db.unsafeRaw(RUN_FORGET_CLEANUP_REASON);
39
+ const runner = ctx.db.unsafeRaw();
39
40
  // Operator-triggered forget must also erase binaries, not just rows —
40
41
  // it flips users to Deleted, after which the cron never re-processes
41
42
  // them, so a row-only delete here would permanently leak the binaries.
@@ -18,7 +18,7 @@ import { USER_STATUS, userEntity, userTable } from "../../user/index.js";
18
18
  // NICHT `event.user.tenantId` (das ist der aktive Tenant zur Lifecycle-Zeit und
19
19
  // kann abweichen).
20
20
  const userExecutor = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
21
- // `conn` is ctx.db.unsafeRaw(reason) (regular handlers) or the open tx
21
+ // `conn` is ctx.db.unsafeRaw() (regular handlers) or the open tx
22
22
  // (forget-cleanup sub-tx) — keeps the event append atomic with the write.
23
23
  //
24
24
  // Returns `{ applied: false }` instead of throwing only when `options.expect`
@@ -1,11 +1,13 @@
1
1
  // Forget-Cleanup-Runner (S2.U5b) — pure-Function Pipeline.
2
2
  //
3
- // Nach abgelaufener Grace-Period (S2.U5a setzt gracePeriodEnd) iteriert
4
- // dieser Runner ueber alle User in DeletionRequested-State und triggert
5
- // die EXT_USER_DATA-delete-Hooks pro Membership-Tenant.
3
+ // After the grace period (S2.U5a sets gracePeriodEnd) this runner iterates
4
+ // over all users in DeletionRequested state and triggers the EXT_USER_DATA
5
+ // delete hooks in every tenant the user is or was a member of (membership
6
+ // history from kumiko_events).
6
7
  //
7
- // **Cross-Tenant-Iteration:** Ein User-Forget-Antrag in Tenant A muss
8
- // die Daten des Users in ALLEN seinen Tenants entfernen — siehe
8
+ // **Cross-tenant iteration:** a forget request in tenant A must remove the
9
+ // user's data in ALL of their tenants, including tenants they left before
10
+ // the request — see
9
11
  // docs/plans/architecture/user-data-rights.md "Cross-Tenant-Semantik".
10
12
  //
11
13
  // **Strategy-Dispatch:** Pro Entity entscheidet die data-retention-
@@ -177,15 +179,16 @@ async function processUser(args) {
177
179
  // memberships in der Tx loeschen. Orphan-User (0 memberships) liefert
178
180
  // [] in Email-args; App-Author-Template kann das case-handlen.
179
181
  const tenantIdsBeforeDelete = memberships.map((m) => m.tenantId);
180
- // Edge-Case "0 Memberships": User hat alle Tenants schon verlassen
181
- // bevor Forget triggerte. Wir laufen den Hook-Loop trotzdem mit einem
182
- // Pseudo-Tenant — der user-Hook (user-data-rights-defaults) ist
183
- // tenant-agnostisch und MUSS laufen damit email/displayName/passwordHash
184
- // anonymisiert werden. Tenant-scoped Hooks (z.B. fileRefDeleteHook)
185
- // finden im Pseudo-Tenant nichts und sind no-op. Ohne diesen Pfad
186
- // wuerde status=Deleted gesetzt waehrend Original-PII liegen bleibt
187
- // — sieht compliant aus, ist es nicht (advisor-Finding S2.U5b.fix1).
188
- const tenantList = memberships.length > 0 ? memberships.map((m) => m.tenantId) : [SYSTEM_TENANT_ID_FOR_ORPHANS];
182
+ // Departed tenants: removeMember deletes only the projection row, so data
183
+ // the user left behind there (e.g. note mentions) must still be reached.
184
+ const historicalTenantIds = await readHistoricalMembershipTenantIds(db, userId);
185
+ const hookTenantIds = [...new Set([...tenantIdsBeforeDelete, ...historicalTenantIds])];
186
+ // Edge case "0 tenants" (neither membership nor history): the hook loop still
187
+ // runs with a pseudo tenant. The user hook is tenant-agnostic and MUST run so
188
+ // email/displayName/passwordHash get anonymized; tenant-scoped hooks find
189
+ // nothing there and no-op. Without this path status=Deleted would be set
190
+ // while the original PII stays — compliant-looking, but not compliant.
191
+ const tenantList = hookTenantIds.length > 0 ? hookTenantIds : [SYSTEM_TENANT_ID_FOR_ORPHANS];
189
192
  // Per-User-Sub-Tx: hooks + status-flip atomar. Bei Hook-Throw rollt
190
193
  // nur dieser User zurueck, andere User bleiben commit-fest. Die Sub-Tx
191
194
  // nestet korrekt: eine Top-Level-Connection oeffnet sie via `.begin`
@@ -342,7 +345,7 @@ async function resolveEffectiveTenantModel(db, tenantId, appTenantModel) {
342
345
  // rows.
343
346
  //
344
347
  // Filter by payload->>'tenantId' (not the event column tenant_id): cross-
345
- // tenant SystemAdmin adds land under the actor's tenant_id (#2347).
348
+ // tenant SystemAdmin adds land under the actor's tenant_id.
346
349
  //
347
350
  // Counting created-ROWS against distinct userIds latched a tenant to
348
351
  // multi-user as soon as its sole member was removed and re-added (#2608) —
@@ -369,6 +372,18 @@ async function everHadMultipleMembers(db, tenantId) {
369
372
  // could name a second member.
370
373
  return row.event_count > row.readable_users;
371
374
  }
375
+ // Filter by payload (not the event column tenant_id) for the same reason as
376
+ // everHadMultipleMembers: cross-tenant SystemAdmin adds are billed to the
377
+ // actor's tenant.
378
+ async function readHistoricalMembershipTenantIds(db, userId) {
379
+ const rows = await executeRawQuery(db, `SELECT DISTINCT payload->>'tenantId' AS tenant_id
380
+ FROM kumiko_events
381
+ WHERE aggregate_type = 'tenant-membership'
382
+ AND type = $1
383
+ AND payload->>'userId' = $2
384
+ AND NULLIF(payload->>'tenantId', '') IS NOT NULL`, [entityEventName("tenant-membership", "created"), userId]);
385
+ return rows.map((row) => row.tenant_id); // @cast-boundary db-row
386
+ }
372
387
  // Mapping retention.strategy → user-data-rights.UserDataDeleteStrategy.
373
388
  // - "anonymize" / "blockDelete" → "anonymize" (Aufbewahrungs-Pflicht
374
389
  // blockDelete: Daten muessen physisch bleiben, nur Personen-Bezug raus)
@@ -20,7 +20,7 @@
20
20
  // This subscriber never resumes a run itself (D3, workflow-resume-loop.md):
21
21
  // the MultiStreamApplyContext it runs in has no callFeature/runStepList
22
22
  // access, only unsafeAppendEvent/loadAggregate. It writes triggerEventType
23
- // + triggerPayload + wakeAt=now() onto the matching pending row(s); the
23
+ // + triggerEventRef + wakeAt=now() onto the matching pending row(s); the
24
24
  // existing resume-due-runs job (Phase 2) picks the row up on its next
25
25
  // cron tick — the one resume path stays exactly the one Phase 2 built.
26
26
  import { selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
@@ -66,7 +66,11 @@ const wakeupApply = async (event, tx) => {
66
66
  continue;
67
67
  await updateMany(tx, workflowRunPendingTable, {
68
68
  triggerEventType: event.type,
69
- triggerPayload: event.payload,
69
+ triggerEventRef: {
70
+ eventId: event.id,
71
+ aggregateId: event.aggregateId,
72
+ version: event.version,
73
+ },
70
74
  wakeAt: getTemporal().Now.instant().toString(),
71
75
  }, { tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex });
72
76
  }
@@ -69,6 +69,11 @@ export function registerEventTrigger(r, workflow) {
69
69
  runId,
70
70
  workflow,
71
71
  triggerEvent,
72
+ triggerEventRef: {
73
+ eventId: event.id,
74
+ aggregateId: event.aggregateId,
75
+ version: event.version,
76
+ },
72
77
  ...(idempotencyKey && { idempotencyKey }),
73
78
  // @cast-boundary msp-to-handler-ctx — MultiStreamApplyContext only
74
79
  // exposes unsafeAppendEvent/loadAggregate, a subset of