@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
@@ -16,16 +16,19 @@
16
16
  // otherwise (wait already wrote its effect; resume past it).
17
17
  //
18
18
  // The run's ORIGINAL trigger event (the one that started the whole run) is
19
- // always recovered from the run's own WORKFLOW_RUN_STARTED_TYPE event,
20
- // never from the pending row — that event always carries it (see
21
- // WorkflowRunStartedPayload in ./runner). The pending row's own
22
- // triggerEventType/triggerPayload columns are a DIFFERENT thing: for a
23
- // waitForEvent suspension they hold the AWAITED event the Phase 3b
19
+ // always re-read from the event store via the triggerEventRef on the run's
20
+ // own WORKFLOW_RUN_STARTED_TYPE event, never from the pending row (see
21
+ // WorkflowRunStartedPayload in ./runner). Neither the run stream nor the
22
+ // pending row holds a copy of a foreign payload, so an erased source event
23
+ // shows up here in its shredded form, and an unloadable one (archived
24
+ // stream) fails the run. The pending row's own
25
+ // triggerEventType/triggerEventRef columns are a DIFFERENT thing: for a
26
+ // waitForEvent suspension they point at the AWAITED event the Phase 3b
24
27
  // event-subscriber matched (NULL until then, and NULL forever on a
25
- // timeout-without-a-match). That payload becomes the resumed pipeline's
26
- // result for the skipped waitForEvent step itself (see resultKey on
27
- // steps/wait-for-event.ts) — pre-seeded into stepsAcc below so a
28
- // subsequent step's resolver can read `ctx.steps[awaits.someKey]`.
28
+ // timeout-without-a-match). That event's payload becomes the resumed
29
+ // pipeline's result for the skipped waitForEvent step itself (see
30
+ // resultKey on steps/wait-for-event.ts) — pre-seeded into stepsAcc below so
31
+ // a subsequent step's resolver can read `ctx.steps[awaits.someKey]`.
29
32
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
30
33
  import { buildPipelineSteps, computeDefinitionFingerprint, describeWorkflowStepError, getStep, runStepList, SYSTEM_ROLE, WORKFLOW_AGGREGATE_TYPE, WORKFLOW_RESUMED_TYPE, WORKFLOW_RETRY_SCHEDULED_TYPE, WORKFLOW_RUN_COMPLETED_TYPE, WORKFLOW_RUN_FAILED_TYPE, WORKFLOW_RUN_STARTED_TYPE, WORKFLOW_WAITING_FOR_EVENT_TYPE, } from "@cosmicdrift/kumiko-framework/engine";
31
34
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
@@ -113,40 +116,134 @@ function isRunAlreadySettled(runEvents, stepIndex, expectedRetryAttempt) {
113
116
  return resumed.stepIndex === stepIndex && resumed.retryAttempt === expectedRetryAttempt;
114
117
  });
115
118
  }
116
- function recoverTriggerEvent(runEvents, runId, user) {
119
+ function isRecord(value) {
120
+ return typeof value === "object" && value !== null;
121
+ }
122
+ function isTriggerEventRef(value) {
123
+ return (isRecord(value) &&
124
+ typeof value["eventId"] === "string" &&
125
+ typeof value["aggregateId"] === "string" &&
126
+ typeof value["version"] === "number");
127
+ }
128
+ function readTriggerSource(startedPayload) {
129
+ if (!isRecord(startedPayload) || typeof startedPayload["triggerEventType"] !== "string") {
130
+ return null;
131
+ }
132
+ const eventType = startedPayload["triggerEventType"];
133
+ const ref = startedPayload["triggerEventRef"];
134
+ if (isTriggerEventRef(ref)) {
135
+ return { kind: "ref", eventType, ref };
136
+ }
137
+ if ("triggerPayload" in startedPayload) {
138
+ return { kind: "legacy-copy", eventType, payload: startedPayload["triggerPayload"] };
139
+ }
140
+ return null;
141
+ }
142
+ async function loadReferencedEvent(ctx, ref) {
143
+ const events = await ctx.loadAggregate(ref.aggregateId);
144
+ return events.find((e) => e.id === ref.eventId);
145
+ }
146
+ async function recoverTriggerEvent(ctx, runEvents, runId, user) {
117
147
  const started = runEvents.find((e) => e.type === WORKFLOW_RUN_STARTED_TYPE);
118
148
  if (!started) {
119
149
  throw new InternalError({
120
150
  message: `workflow-runner:write:resume-run: run ${runId} has no ${WORKFLOW_RUN_STARTED_TYPE} event — cannot recover its trigger event.`,
121
151
  });
122
152
  }
123
- const startedPayload = started.payload; // @cast-boundary event-store-payload
124
- const triggerEvent = {
125
- type: startedPayload.triggerEventType,
126
- payload: startedPayload.triggerPayload,
127
- user,
153
+ const source = readTriggerSource(started.payload);
154
+ if (!source) {
155
+ throw new InternalError({
156
+ message: `workflow-runner:write:resume-run: run ${runId} has a malformed ${WORKFLOW_RUN_STARTED_TYPE} event — neither triggerEventRef nor a legacy triggerPayload.`,
157
+ });
158
+ }
159
+ if (source.kind === "legacy-copy") {
160
+ return {
161
+ event: { type: source.eventType, payload: source.payload, user },
162
+ unavailable: false,
163
+ };
164
+ }
165
+ const triggerEvent = await loadReferencedEvent(ctx, source.ref);
166
+ if (!triggerEvent) {
167
+ return { event: null, unavailable: true };
168
+ }
169
+ return {
170
+ event: { type: source.eventType, payload: triggerEvent.payload, user },
171
+ unavailable: false,
128
172
  };
129
- return triggerEvent;
173
+ }
174
+ // NULL ref + NULL payload = timeout without a match: the step result stays
175
+ // undefined, same as before. A matched row whose event can no longer be loaded
176
+ // is NOT folded into that case: a later step could not tell "nothing arrived"
177
+ // from "arrived but gone" and would run on missing data, so the run fails
178
+ // instead. Rows matched before the reference existed still carry the copy.
179
+ async function loadAwaitedPayload(ctx, pending) {
180
+ if (!isTriggerEventRef(pending.triggerEventRef)) {
181
+ return { unavailable: false, payload: pending.triggerPayload ?? undefined };
182
+ }
183
+ const awaited = await loadReferencedEvent(ctx, pending.triggerEventRef);
184
+ return awaited ? { unavailable: false, payload: awaited.payload } : { unavailable: true };
130
185
  }
131
186
  // The suspended waitForEvent step itself is skipped on resume
132
187
  // (resumeFrom = stepIndex + 1) — its run() never re-executes, so its
133
188
  // resultKey (args.event, see steps/wait-for-event.ts) never gets
134
- // populated by the normal runStepList loop. Seed it here from the
135
- // pending row's own triggerPayload so a subsequent step's resolver
136
- // sees the matched event via `ctx.steps[awaits.someKey]`, same as any
137
- // other step result. NULL on a timeout-without-a-match — a later
138
- // resolver reading it just sees `undefined`, no special-casing needed.
139
- function seedResumedStepResults(pending, steps, stepIndex) {
189
+ // populated by the normal runStepList loop. Seed it here from the awaited
190
+ // event's payload so a subsequent step's resolver sees the matched event via
191
+ // `ctx.steps[awaits.someKey]`, same as any other step result.
192
+ function seedResumedStepResults(suspensionEventType, awaitedPayload, steps, stepIndex) {
140
193
  const stepsAcc = {};
141
- if (pending.suspensionEventType === WORKFLOW_WAITING_FOR_EVENT_TYPE) {
194
+ if (suspensionEventType === WORKFLOW_WAITING_FOR_EVENT_TYPE) {
142
195
  const suspendedStep = steps[stepIndex];
143
196
  const key = suspendedStep && getStep(suspendedStep.kind)?.resultKey?.(suspendedStep.args);
144
197
  if (key !== undefined) {
145
- stepsAcc[key] = pending.triggerPayload;
198
+ stepsAcc[key] = awaitedPayload;
146
199
  }
147
200
  }
148
201
  return stepsAcc;
149
202
  }
203
+ async function runResumedPipeline(ctx, input) {
204
+ const { workflow, workflowName, runId, stepIndex, resumeFrom, triggerEvent, awaitedPayload, pending, } = input;
205
+ try {
206
+ const steps = buildPipelineSteps(workflow.pipelineDef, triggerEvent);
207
+ const workflowCtx = {
208
+ runId,
209
+ workflowName,
210
+ stepIndex,
211
+ definitionFingerprint: pending.definitionFingerprint ?? undefined,
212
+ ...(pending.retryAttempt !== null && { retryAttempt: pending.retryAttempt + 1 }),
213
+ };
214
+ const stepsAcc = seedResumedStepResults(pending.suspensionEventType, awaitedPayload, steps, stepIndex);
215
+ const outcome = await runStepList(steps, triggerEvent, ctx, stepsAcc, {}, workflowCtx, resumeFrom);
216
+ if (outcome.kind === "suspended") {
217
+ if (!isResumableSuspension(steps, outcome.stepIndex)) {
218
+ throw new WorkflowSuspensionUnsupportedError(workflowName, outcome.stepIndex);
219
+ }
220
+ // Another suspension further down the pipeline — pending-projection.ts
221
+ // already materialised the new row; nothing more to do this pass.
222
+ return { isSuccess: true, data: { outcome: "suspended" } };
223
+ }
224
+ const completedPayload = {
225
+ workflowName,
226
+ stepIndex: steps.length,
227
+ };
228
+ await ctx.unsafeAppendEvent({
229
+ aggregateId: runId,
230
+ aggregateType: WORKFLOW_AGGREGATE_TYPE,
231
+ type: WORKFLOW_RUN_COMPLETED_TYPE,
232
+ payload: completedPayload,
233
+ });
234
+ return { isSuccess: true, data: { outcome: "completed" } };
235
+ }
236
+ catch (error) {
237
+ log.warn("workflow run failed", { runId, workflowName, stepIndex, error: String(error) });
238
+ const failedPayload = {
239
+ workflowName,
240
+ stepIndex,
241
+ error: describeWorkflowStepError(error),
242
+ };
243
+ await appendRunFailed(ctx, runId, failedPayload);
244
+ return { isSuccess: true, data: { outcome: "failed" } };
245
+ }
246
+ }
150
247
  export const resumeRunHandler = {
151
248
  name: "resume-run",
152
249
  agent: { expose: false },
@@ -198,48 +295,39 @@ export const resumeRunHandler = {
198
295
  // pipeline; nothing left for this call to do.
199
296
  return { isSuccess: true, data: { outcome: "already-resumed" } };
200
297
  }
201
- const triggerEvent = recoverTriggerEvent(runEvents, runId, event.user);
202
- const resumeFrom = pending.suspensionEventType === WORKFLOW_RETRY_SCHEDULED_TYPE ? stepIndex : stepIndex + 1;
203
- try {
204
- const steps = buildPipelineSteps(workflow.pipelineDef, triggerEvent);
205
- const workflowCtx = {
206
- runId,
298
+ const recovered = await recoverTriggerEvent(ctx, runEvents, runId, event.user);
299
+ if (recovered.unavailable) {
300
+ await appendRunFailed(ctx, runId, {
207
301
  workflowName,
208
302
  stepIndex,
209
- definitionFingerprint: pending.definitionFingerprint ?? undefined,
210
- ...(pending.retryAttempt !== null && { retryAttempt: pending.retryAttempt + 1 }),
211
- };
212
- const stepsAcc = seedResumedStepResults(pending, steps, stepIndex);
213
- const outcome = await runStepList(steps, triggerEvent, ctx, stepsAcc, {}, workflowCtx, resumeFrom);
214
- if (outcome.kind === "suspended") {
215
- if (!isResumableSuspension(steps, outcome.stepIndex)) {
216
- throw new WorkflowSuspensionUnsupportedError(workflowName, outcome.stepIndex);
217
- }
218
- // Another suspension further down the pipeline — pending-projection.ts
219
- // already materialised the new row; nothing more to do this pass.
220
- return { isSuccess: true, data: { outcome: "suspended" } };
221
- }
222
- const completedPayload = {
223
- workflowName,
224
- stepIndex: steps.length,
225
- };
226
- await ctx.unsafeAppendEvent({
227
- aggregateId: runId,
228
- aggregateType: WORKFLOW_AGGREGATE_TYPE,
229
- type: WORKFLOW_RUN_COMPLETED_TYPE,
230
- payload: completedPayload,
303
+ error: `Trigger event of run ${runId} is no longer available — cannot resume.`,
304
+ reason: "trigger_event_unavailable",
231
305
  });
232
- return { isSuccess: true, data: { outcome: "completed" } };
306
+ return { isSuccess: true, data: { outcome: "failed" } };
233
307
  }
234
- catch (error) {
235
- log.warn("workflow run failed", { runId, workflowName, stepIndex, error: String(error) });
236
- const failedPayload = {
308
+ const triggerEvent = recovered.event;
309
+ const awaited = pending.suspensionEventType === WORKFLOW_WAITING_FOR_EVENT_TYPE
310
+ ? await loadAwaitedPayload(ctx, pending)
311
+ : { unavailable: false, payload: undefined };
312
+ if (awaited.unavailable) {
313
+ await appendRunFailed(ctx, runId, {
237
314
  workflowName,
238
315
  stepIndex,
239
- error: describeWorkflowStepError(error),
240
- };
241
- await appendRunFailed(ctx, runId, failedPayload);
316
+ error: `Awaited event of run ${runId} is no longer available — cannot resume.`,
317
+ reason: "awaited_event_unavailable",
318
+ });
242
319
  return { isSuccess: true, data: { outcome: "failed" } };
243
320
  }
321
+ const resumeFrom = pending.suspensionEventType === WORKFLOW_RETRY_SCHEDULED_TYPE ? stepIndex : stepIndex + 1;
322
+ return runResumedPipeline(ctx, {
323
+ workflow,
324
+ workflowName,
325
+ runId,
326
+ stepIndex,
327
+ resumeFrom,
328
+ triggerEvent,
329
+ awaitedPayload: awaited.payload,
330
+ pending,
331
+ });
244
332
  },
245
333
  };
@@ -27,6 +27,7 @@ async function upsertPending(tx, row) {
27
27
  // Phase 3's event-subscriber writes these when the awaited event
28
28
  // arrives — Phase 1 never touches them, insert or update.
29
29
  triggerEventType: null,
30
+ triggerEventRef: null,
30
31
  triggerPayload: null,
31
32
  }, { conflictKeys: ["tenantId", "runId", "stepIndex"] });
32
33
  }
@@ -1,9 +1,14 @@
1
1
  import type { HandlerContext, StepInstance, WorkflowDefinition, WriteEvent } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare function isResumableSuspension(steps: readonly StepInstance[], stepIndex: number): boolean;
3
+ export type TriggerEventRef = {
4
+ readonly eventId: string;
5
+ readonly aggregateId: string;
6
+ readonly version: number;
7
+ };
3
8
  export type WorkflowRunStartedPayload = {
4
9
  readonly workflowName: string;
5
10
  readonly triggerEventType: string;
6
- readonly triggerPayload: unknown;
11
+ readonly triggerEventRef: TriggerEventRef;
7
12
  readonly definitionFingerprint: string;
8
13
  readonly idempotencyKey?: string;
9
14
  };
@@ -35,6 +40,7 @@ export declare function startAndRunWorkflow(args: {
35
40
  readonly runId: string;
36
41
  readonly workflow: WorkflowDefinition;
37
42
  readonly triggerEvent: WriteEvent;
43
+ readonly triggerEventRef: TriggerEventRef;
38
44
  readonly idempotencyKey?: string;
39
45
  readonly handlerCtx: HandlerContext;
40
46
  }): Promise<{
@@ -42,7 +42,7 @@ export async function startAndRunWorkflow(args) {
42
42
  const startedPayload = {
43
43
  workflowName: args.workflow.name,
44
44
  triggerEventType: args.triggerEvent.type,
45
- triggerPayload: args.triggerEvent.payload,
45
+ triggerEventRef: args.triggerEventRef,
46
46
  definitionFingerprint: fingerprint,
47
47
  ...(args.idempotencyKey && { idempotencyKey: args.idempotencyKey }),
48
48
  };
@@ -38,8 +38,14 @@ export const workflowRunPendingTable = pgTable("workflow_run_pending", {
38
38
  waitEventType: text("wait_event_type"),
39
39
  matchExpr: jsonb("match_expr"),
40
40
  // Written by the Phase 3 event-subscriber when the awaited event
41
- // arrives — always NULL in Phase 1/2.
41
+ // arrives — always NULL in Phase 1/2. The row points at the awaited event
42
+ // ({ eventId, aggregateId, version }); resume-run re-reads it from the
43
+ // event store, so erasing the event also covers the workflow.
42
44
  triggerEventType: text("trigger_event_type"),
45
+ triggerEventRef: jsonb("trigger_event_ref"),
46
+ // Legacy copy of the awaited payload. No longer written; only rows
47
+ // matched before the reference existed still carry it, and resume-run
48
+ // falls back to it for those.
43
49
  triggerPayload: jsonb("trigger_payload"),
44
50
  }, (t) => [
45
51
  primaryKey({
@@ -65,6 +71,7 @@ export const workflowRunPendingTableMeta = defineUnmanagedTable({
65
71
  { name: "wait_event_type", pgType: "text", notNull: false },
66
72
  { name: "match_expr", pgType: "jsonb", notNull: false },
67
73
  { name: "trigger_event_type", pgType: "text", notNull: false },
74
+ { name: "trigger_event_ref", pgType: "jsonb", notNull: false },
68
75
  { name: "trigger_payload", pgType: "jsonb", notNull: false },
69
76
  ],
70
77
  indexes: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.337.1",
3
+ "version": "0.339.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>",
@@ -365,6 +365,22 @@
365
365
  "types": "./dist/channel-push/index.d.ts",
366
366
  "default": "./dist/channel-push/index.js"
367
367
  },
368
+ "./channel-slack": {
369
+ "types": "./dist/channel-slack/index.d.ts",
370
+ "default": "./dist/channel-slack/index.js"
371
+ },
372
+ "./channel-discord": {
373
+ "types": "./dist/channel-discord/index.d.ts",
374
+ "default": "./dist/channel-discord/index.js"
375
+ },
376
+ "./channel-teams": {
377
+ "types": "./dist/channel-teams/index.d.ts",
378
+ "default": "./dist/channel-teams/index.js"
379
+ },
380
+ "./channel-telegram": {
381
+ "types": "./dist/channel-telegram/index.d.ts",
382
+ "default": "./dist/channel-telegram/index.js"
383
+ },
368
384
  "./renderer-simple": {
369
385
  "types": "./dist/renderer-simple/index.d.ts",
370
386
  "default": "./dist/renderer-simple/index.js"
@@ -483,12 +499,12 @@
483
499
  }
484
500
  },
485
501
  "dependencies": {
486
- "@cosmicdrift/kumiko-dispatcher-live": "0.337.1",
487
- "@cosmicdrift/kumiko-framework": "0.337.1",
488
- "@cosmicdrift/kumiko-headless": "0.337.1",
489
- "@cosmicdrift/kumiko-renderer": "0.337.1",
490
- "@cosmicdrift/kumiko-renderer-web": "0.337.1",
491
- "@cosmicdrift/kumiko-types": "0.337.1",
502
+ "@cosmicdrift/kumiko-dispatcher-live": "0.339.0",
503
+ "@cosmicdrift/kumiko-framework": "0.339.0",
504
+ "@cosmicdrift/kumiko-headless": "0.339.0",
505
+ "@cosmicdrift/kumiko-renderer": "0.339.0",
506
+ "@cosmicdrift/kumiko-renderer-web": "0.339.0",
507
+ "@cosmicdrift/kumiko-types": "0.339.0",
492
508
  "@mollie/api-client": "^4.5.0",
493
509
  "@node-rs/argon2": "^2.0.2",
494
510
  "@types/mailparser": "^3.4.6",
@@ -859,6 +875,22 @@
859
875
  "types": "./dist/channel-push/index.d.ts",
860
876
  "default": "./dist/channel-push/index.js"
861
877
  },
878
+ "./channel-slack": {
879
+ "types": "./dist/channel-slack/index.d.ts",
880
+ "default": "./dist/channel-slack/index.js"
881
+ },
882
+ "./channel-discord": {
883
+ "types": "./dist/channel-discord/index.d.ts",
884
+ "default": "./dist/channel-discord/index.js"
885
+ },
886
+ "./channel-teams": {
887
+ "types": "./dist/channel-teams/index.d.ts",
888
+ "default": "./dist/channel-teams/index.js"
889
+ },
890
+ "./channel-telegram": {
891
+ "types": "./dist/channel-telegram/index.d.ts",
892
+ "default": "./dist/channel-telegram/index.js"
893
+ },
862
894
  "./renderer-simple": {
863
895
  "types": "./dist/renderer-simple/index.d.ts",
864
896
  "default": "./dist/renderer-simple/index.js"
@@ -985,8 +1017,8 @@
985
1017
  ],
986
1018
  "devDependencies": {
987
1019
  "@testing-library/user-event": "^14.6.1",
988
- "@cosmicdrift/kumiko-locale-de": "0.337.1",
989
- "@cosmicdrift/kumiko-locale-es": "0.337.1",
1020
+ "@cosmicdrift/kumiko-locale-de": "0.339.0",
1021
+ "@cosmicdrift/kumiko-locale-es": "0.339.0",
990
1022
  "jsqr": "^1.4.0"
991
1023
  }
992
1024
  }
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "0.338.0",
4
+ "type": "improvement",
5
+ "title": "entity get/list tools honour agent.risk, PII-bearing system lists are high risk",
6
+ "detail": "agent-tools: `get_<entity>` and `list_<entity>` take their risk from the entity handler's `agent.risk` instead of a hard-coded `low`. The SystemAdmin `user:list`, `user:detail` and `download-attempt:list` handlers declare `risk: \"high\"`.",
7
+ "migration": "No action needed: handlers without an agent.risk hint stay low risk."
8
+ },
9
+ {
10
+ "version": "0.338.0",
11
+ "type": "fix",
12
+ "title": "agent entity tools no longer filter on read-restricted fields",
13
+ "detail": "agent-tools: `list_<entity>` filters and `find_<entity>_by_<field>` tools only offer filterable fields that every caller role may read without a row condition. A role- or ownership-restricted filterable field can no longer be probed through `totalCount` or hit/miss results.",
14
+ "migration": "No action needed: fields whose access.read is not unconditionally open to the agent's roles drop out of list filters and find-by tools."
15
+ },
2
16
  {
3
17
  "version": "0.337.0",
4
18
  "type": "fix",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.339.0",
4
+ "type": "improvement",
5
+ "title": "emailPasswordClient({ loginUrl, postLogoutUrl }) sends unauthenticated visitors to an external login page with a same-origin next path and navigates there on logout",
6
+ "detail": "emailPasswordClient accepts loginUrl and postLogoutUrl for external login pages",
7
+ "migration": "No code change needed."
8
+ },
2
9
  {
3
10
  "version": "0.335.0",
4
11
  "type": "fix",
@@ -1,4 +1,87 @@
1
1
  [
2
+ {
3
+ "version": "0.339.0",
4
+ "type": "improvement",
5
+ "title": "Missing or unchecked checkout consent returns 422 consent_required",
6
+ "detail": "The consent payload is optional in the `start-plan-checkout` and `create-checkout-session` input schemas and its two flags are plain booleans. `prepareConsent` throws `UnprocessableError(\"consent_required\")` with `billing-foundation.errors.consentRequired` when consumer protection is on and the consent is missing or either flag is not true.",
7
+ "migration": "Clients that treated the previous `400` validation error for a missing consent as the signal must check for `422` with `reason: \"consent_required\"`."
8
+ },
9
+ {
10
+ "version": "0.339.0",
11
+ "type": "fix",
12
+ "title": "Contract confirmation, termination receipt and cancellation pages use \"du\"",
13
+ "detail": "The German contract confirmation mail, the termination and withdrawal receipt mail, the public cancellation pages and the subscription and payment submit messages address the customer with \"du\". The recorded consent statements are first-person and unchanged, so `consentTextVersion` stays the same."
14
+ },
15
+ {
16
+ "version": "0.339.0",
17
+ "type": "fix",
18
+ "title": "The § 312k confirm POST forwards the visitor's host to /api/write",
19
+ "detail": "The `/legal/kuendigen` and `/legal/cancel` pages re-enter `/api/write` through the app. The re-entry now carries `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Tenant` and the `kumiko_tenant` cookie as the visitor sent them, so a host-based `tenantResolver` resolves the same tenant as for a direct call; before, it saw no host and every confirm failed with `400 tenant_required`. The session cookie, `Authorization` and `X-Forwarded-For` are not forwarded.",
20
+ "migration": "keine"
21
+ },
22
+ {
23
+ "version": "0.339.0",
24
+ "type": "improvement",
25
+ "title": "consumerProtection.terminationScope \"platform\" for hosts without a tenant",
26
+ "detail": "`consumerProtection.terminationScope` is `\"tenant-host\"` (default, unchanged) or `\"platform\"`. With `\"platform\"`, `request-contract-termination` is flagged `tenantlessAnonymous` and also runs on a host whose resolver returns no tenant: it finds the contract by the declarant's email as before, records on the matched tenant's subscription stream, and answers every outcome identically. An unknown option value fails at `createBillingFoundationFeature`.",
27
+ "migration": "Apps that serve the pages on a platform apex (publicstatus) set `consumerProtection.terminationScope: \"platform\"`. Other apps change nothing."
28
+ },
29
+ {
30
+ "version": "0.339.0",
31
+ "type": "improvement",
32
+ "title": "Public termination declarations cancel at the provider in a job",
33
+ "detail": "For the public channel, `request-contract-termination` no longer calls the provider in the request. A matched request appends the PII-free `contract-termination-declared` event (`requestId`, `declarationType`, `terminationKind`, `receivedAtIso`, `locale`) via the system-only `declare-contract-termination`; the job `cancel-on-public-termination-declared` then runs `record-contract-termination`, which asks the provider for `cancel_at_period_end` and appends `contract-termination-requested` with the outcome. Matched and unmatched requests do the same work in the request, so the response time does not reveal whether the email is a customer. The job is idempotent per `requestId`. A provider without `cancelSubscription`, a provider error or a missing subscription now sends a separate operator notice with request id and tenant id but no name or email. A public withdrawal still makes no provider call. The account path (`terminate-contract`) stays synchronous.",
34
+ "migration": "Code that expected `contract-termination-requested` right after the public request must wait for the job (in tests: `drainJobs`)."
35
+ },
36
+ {
37
+ "version": "0.338.0",
38
+ "type": "improvement",
39
+ "title": "Contract confirmation mail after a recorded checkout consent",
40
+ "detail": "With `consumerProtection`, four event-triggered jobs (`confirm-on-subscription-created`, `confirm-on-subscription-updated`, `confirm-on-invoice-paid`, `confirm-on-payment-received`) call the system-only `billing-foundation:write:issue-contract-confirmation` (`consentId`, `sourceAggregateId`, optional `currentPeriodEndIso`) for events carrying a `consentId`; subscription events only count with status `active` or `trialing`. The handler only reads the caller tenant's own subscription or payment stream, finds the `checkout-consent-recorded` event, appends `contract-confirmation-issued` (`consentId`, `issuedAtIso`, `locale`, `termsTemplateVersion`) at the fetched stream version and then notifies `billing-foundation:contract-confirmation` (critical priority) to the consenting user's email. A second run returns `{ issued: false, reason: \"already_issued\" }` or fails with a version conflict; an unknown consent returns `consent_not_found` and sends nothing. The mail is rendered in German or English (fallback German) as structured header/sections content for the email channel's renderer.",
41
+ "migration": "Enabling `consumerProtection` additionally requires the `user` feature, and an email channel plus renderer must be mounted for the mail to go out."
42
+ },
43
+ {
44
+ "version": "0.338.0",
45
+ "type": "improvement",
46
+ "title": "Optional consumer-protection consent gate on checkout",
47
+ "detail": "`BillingFoundationOptions.consumerProtection` (`termsTextBlock`, `vatNote` with `de` and `en`, `operatorEmail`, `legalLinks`) requires `baseUrl` and additionally requires the `template-resolver` and `delivery` features. When set, `start-plan-checkout` and `create-checkout-session` take a strict `consent` object (`earlyPerformanceRequested: true`, `withdrawalLossAcknowledged: true`, `consentTextVersion`, `locale`); a missing or unchecked consent fails validation (400). The consent texts and their version (first 16 hex characters of the SHA-256 of the early-performance and withdrawal-loss texts) are owned by the framework in German and English. After the provider returned the checkout URL, `checkout-consent-recorded` is appended (no email, no IP). `billing-plans` returns `consumerProtection: { consentTexts, legalLinks }` so a client can render and echo the version.",
48
+ "migration": "No code change needed unless you enable consumerProtection; then callers of start-plan-checkout/create-checkout-session must send `consent`."
49
+ },
50
+ {
51
+ "version": "0.338.0",
52
+ "type": "improvement",
53
+ "title": "Consent dialog and cancel-contract button in the billing panel",
54
+ "detail": "The checkout CTA opens `CheckoutConsentDialog` when `billing-plans` returns `consumerProtection`; `start-plan-checkout` then receives `consent: { earlyPerformanceRequested, withdrawalLossAcknowledged, consentTextVersion, locale }`. The locale rule (`resolveConsentLocale`) moved to the crypto-free `consumer-protection/consent-locale.ts` and is still re-exported from `consent-text.ts`. `CancelContractDialog` calls `billing-foundation:write:terminate-contract`. New English i18n keys under `billing-foundation.consent.*` and `billing-foundation.cancel.*`.",
55
+ "migration": "No action. Apps that translate the panel add the new `billing-foundation.consent.*` and `billing-foundation.cancel.*` keys to their locale bundles."
56
+ },
57
+ {
58
+ "version": "0.338.0",
59
+ "type": "breaking",
60
+ "title": "SubscriptionProviderPlugin.cancelSubscription takes { providerSubscriptionId, when }",
61
+ "detail": "`cancelSubscription(ctx, { providerSubscriptionId, when })` replaces the positional `providerSubscriptionId` argument. `when` is `SubscriptionCancelTimings.periodEnd` (`\"period-end\"`, the subscription runs to the end of the paid period) or `SubscriptionCancelTimings.immediately` (`\"immediately\"`). `createCheckoutSession` options gain optional `consentId`, `locale` and `submitMessage`; `SubscriptionEvent` and `PaymentEvent` gain an optional `consentId`, validated by `parseProviderConsentId` (max 100 characters of `A-Z a-z 0-9 _ -`), and `subscription` / `payment` event payloads store it.",
62
+ "migration": "Custom provider plugins that implement `cancelSubscription` change the signature to `(ctx, { providerSubscriptionId, when })` and honor `when`. A provider that can only cancel immediately may treat both values the same or leave the method out. Callers pass `{ providerSubscriptionId, when: SubscriptionCancelTimings.immediately }` to keep the old behavior."
63
+ },
64
+ {
65
+ "version": "0.338.0",
66
+ "type": "improvement",
67
+ "title": "§ 312k cancellation pages and handlers with consumerProtection",
68
+ "detail": "`contract-termination-requested` (on the tenant's subscription stream: `requestId`, `declarationType` termination|withdrawal, `terminationKind` ordinary|extraordinary, `channel` public|account, `receivedAtIso`, `effectiveAtIso`, `providerCancel` period-end|immediately|none) and `contract-termination-unmatched` (system-tenant stream, `matchResult` none|ambiguous) are appended by the system-only `record-contract-termination` and `record-unmatched-contract-termination` handlers. `billing-foundation:write:request-contract-termination` is anonymous and answers `{ requestId, receivedAtIso }` for every outcome; `billing-foundation:write:terminate-contract` (purchase roles) answers `{ requestId, receivedAtIso, effectiveAtIso, providerCancel }`. Provider errors or a provider without `cancelSubscription` still record the declaration (`providerCancel: none`) and notify the operator. `consumerProtection` now also requires the `tenant` and `user` features.",
69
+ "migration": "Add `createContractTerminationRoutes()` from billing-foundation to the app's `extraRoutes`, make sure `anonymousAccess` is configured, and link `/legal/kuendigen` (or `/legal/cancel`) in the page footer."
70
+ },
71
+ {
72
+ "version": "0.338.0",
73
+ "type": "breaking",
74
+ "title": "Remove rawPayload from provider events and event headers",
75
+ "detail": "`rawPayload` is removed from `SubscriptionEvent`, `PaymentEvent`, `ProviderSubscriptionSnapshot`, the `process-event` and `process-payment-event` schemas and `SubscriptionEventHeaders` / `PaymentEventHeaders`. New events carry `providerEventId` and `providerName` in their headers; payment events also carry the optional `providerCheckoutId` (Stripe checkout session id).",
76
+ "migration": "Custom subscription provider plugins must stop returning `rawPayload`; TypeScript flags the removed field. Code that read the checkout session id out of `headers.rawPayload` reads `headers.providerCheckoutId` instead. Events already stored keep their old `rawPayload` header; this release does not rewrite stored events."
77
+ },
78
+ {
79
+ "version": "0.338.0",
80
+ "type": "improvement",
81
+ "title": "late events of a superseded provider subscription no longer overwrite the live row",
82
+ "detail": "billing-foundation: `process-event` drops a non-`created` provider event that belongs to a different subscription than the one the tenant's row currently tracks, while that row still blocks checkout. A late webhook retry of a superseded subscription can no longer flip the live subscription to `canceled`. The webhook answers 200 with `stale: true`.",
83
+ "migration": "No action needed: events of the tracked subscription and `created` events behave as before."
84
+ },
2
85
  {
3
86
  "version": "0.335.0",
4
87
  "type": "breaking",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.338.0",
4
+ "type": "improvement",
5
+ "title": "corrected the documented reason rolling cap booking needs SystemAdmin",
6
+ "detail": "cap-counter: corrected the documented reason why rolling cap booking still needs a `SystemAdmin` identity. The event-ownership check only guards `appendDomainEventCore`, not the entity-executor path, so the earlier \"ownership rule rejects in-process appends\" wording was inaccurate. Behavior is unchanged.",
7
+ "migration": "No action needed: behavior is unchanged."
8
+ },
2
9
  {
3
10
  "version": "0.336.0",
4
11
  "type": "fix",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "0.338.0",
4
+ "type": "breaking",
5
+ "title": "caps:usage and my-caps default to admin roles; widen with usageVisibleTo",
6
+ "detail": "`createCapOverviewFeature` gets `usageVisibleTo?: readonly string[]`. Without it, the `caps:usage` query and the `my-caps` screen accept `access.admin` (TenantAdmin, Admin, SystemAdmin) instead of every tenant member. The screen and the query share the resolved roles. Only built-in roles are accepted (User, Member, Editor, Admin, TenantAdmin, SystemAdmin); an unknown or empty list throws at feature definition. The SystemAdmin `tenantId` override and the platform-wide screens are unchanged. The `MY_CAPS_ACCESS_ROLES` export is replaced by `DEFAULT_CAP_USAGE_ROLES` (the admin default), and `myCapsScreen` by `createMyCapsScreen(roles)`.",
7
+ "migration": "Apps whose regular members should keep seeing their usage pass `createCapOverviewFeature({ ..., usageVisibleTo: [\"User\", \"Editor\", \"Admin\", \"TenantAdmin\", \"SystemAdmin\"] })`. Code that imported `MY_CAPS_ACCESS_ROLES` (handlers or nav entries mirroring the screen rule) imports `DEFAULT_CAP_USAGE_ROLES` for the admin default, or reuses the same `usageVisibleTo` array it passed to the feature."
8
+ },
9
+ {
10
+ "version": "0.338.0",
11
+ "type": "fix",
12
+ "title": "Cross-tenant cap limits read the target tenant's config",
13
+ "detail": "`caps:usage` with a SystemAdmin `tenantId` override and `tenant-caps:list` passed the caller's config accessor to `CapSpec.limit`, so a limit that reads a tenant-scoped config key showed the caller's value for every tenant. Both now resolve one accessor per target tenant through `ctx.configFor`, and `tenant-caps:list` resolves each tenant's limit separately instead of once per tier.",
14
+ "migration": "No code change needed."
15
+ },
2
16
  {
3
17
  "version": "0.335.0",
4
18
  "type": "fix",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.338.0",
4
+ "type": "improvement",
5
+ "title": "config keys can declare an async write validator that rejects with a KumikoError",
6
+ "detail": "Config key definitions accept a new async `validate(value, ctx)` function. `config:write:set` runs it after the type, bounds and pattern checks and before the value is stored, for every scope and backing. A validator rejects the write by throwing a `KumikoError`, for example an `UnprocessableError`. The feature manifest reports `validated` per key.",
7
+ "migration": "No action needed: keys without `validate` behave as before."
8
+ },
2
9
  {
3
10
  "version": "0.335.0",
4
11
  "type": "breaking",
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "version": "0.338.0",
4
+ "type": "improvement",
5
+ "title": "forget-denied audit digest is keyed and omitted without a blind-index key",
6
+ "detail": "crypto-shredding: the forget-denied audit event now stores `subjectKeyDigest` as a keyed blind-index HMAC instead of a bare SHA-256 of the subject key, so a candidate id cannot be matched against it. Without a configured blind-index key the digest is omitted (`subjectKeyDigest` is now optional in the payload).",
7
+ "migration": "No action needed. Existing denial events keep their old digest; configure the blind-index key to get a digest on new ones."
8
+ },
9
+ {
10
+ "version": "0.338.0",
11
+ "type": "fix",
12
+ "title": "forget-subject tenant check falls back to event-store provenance",
13
+ "detail": "forget-subject now accepts a record subject for a DPO when the owning tenant is proven by the event stream, not only by a projection row. A record whose row was already deleted, or a custom aggregate without a registered entity, can be shredded by the DPO of the tenant that owns it. A DPO of another tenant is still denied, and so is any DPO when events for the same aggregate id exist in more than one tenant."
14
+ },
2
15
  {
3
16
  "version": "0.335.0",
4
17
  "type": "fix",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.338.0",
4
+ "type": "fix",
5
+ "title": "field-definition list keeps TenantAdmin when fieldDefinitionWriteRoles is set",
6
+ "detail": "custom-fields: the field-definition list roles always include the default `TenantAdmin`, also when `fieldDefinitionWriteRoles` is set. `TenantAdmin` keeps saving values by default, so the form section must keep loading for it; `fieldDefinitionWriteRoles: []` no longer locks out the list.",
7
+ "migration": "No action needed. Set fieldDefinitionListRoles explicitly to narrow the list roles."
8
+ },
2
9
  {
3
10
  "version": "0.146.0",
4
11
  "type": "improvement",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "0.339.0",
4
+ "type": "improvement",
5
+ "title": "Slack, Discord, Microsoft Teams and Telegram delivery channels",
6
+ "detail": "`channel-slack`, `channel-discord`, `channel-teams` and `channel-telegram` register route-only delivery channels. Webhook URLs and the Telegram bot token live in the tenant's secrets; the send job reads them with an audit entry `<feature>:send`. `DeliveryChannel.resolve` is optional and `ChannelContext.secrets` is available on the send job path.",
7
+ "migration": "keine"
8
+ },
9
+ {
10
+ "version": "0.338.0",
11
+ "type": "improvement",
12
+ "title": "tenant broadcast recipient query no longer reads across tenants through ctx.db",
13
+ "detail": "delivery: the `tenantUserIdsQuery` handler of a tenant broadcast now always gets a tenant-filtered `ctx.db` and `ctx.dbOutsideTransaction`, also when it is declared `r.systemScope()`. A systemScope handler could previously read the memberships of every tenant through `ctx.db` and notify users of foreign tenants. Cross-tenant reads stay available through `ctx.systemDb`.",
14
+ "migration": "No action needed for handlers that resolve recipients through `ctx.systemDb`, like `tenant:query:resolveUserIds`. A systemScope handler that read `ctx.db` unfiltered now sees only the broadcast tenant's rows."
15
+ },
2
16
  {
3
17
  "version": "0.336.0",
4
18
  "type": "fix",