@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
@@ -1,5 +1,7 @@
1
1
  import { fetchOne, runInSavepointIfSupported } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
2
3
  import { decryptStoredPii, denyUnlessJoinRowParentVisible } from "../../shared/index.js";
4
+ import { tenantMembershipsTable } from "../../tenant/index.js";
3
5
  import { userTable } from "../../user/index.js";
4
6
  import { DEFAULT_NOTES_HISTORY_ACCESS } from "../constants.js";
5
7
  import { noteEntryExecutor, noteMentionExecutor } from "../executor.js";
@@ -17,13 +19,14 @@ const READ_AUTHOR_DISPLAY_NAME_REASON = "reads the author's displayName from the
17
19
  // Both the field names and the optional `parents` allowlist come from the
18
20
  // entity's own `parentRef` declaration, which the read gate reads too —
19
21
  // see shared/parent-visibility.ts.
20
- export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS) {
22
+ export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS, entryExecutor = noteEntryExecutor) {
21
23
  return {
22
24
  name: "add-note",
23
25
  schema: addNotePayloadSchema,
24
26
  access,
25
27
  description: "Appends a note to one host entity's history, stamping the author from the authenticated caller rather than the payload; use it for every remark and correction alike, because entries can never be edited or removed afterwards.",
26
28
  escapeHatch: {
29
+ grants: ["unsafeRaw"],
27
30
  reason: READ_AUTHOR_DISPLAY_NAME_REASON,
28
31
  },
29
32
  handler: async (event, ctx) => {
@@ -31,13 +34,27 @@ export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS) {
31
34
  const denied = await denyUnlessJoinRowParentVisible(ctx.registry, "note-entry", payload, event.user, ctx.db);
32
35
  if (denied)
33
36
  return denied;
37
+ // A mention of a non-member would leave a note body about that person out of reach of
38
+ // their forget run, which only visits the tenants they are a member of.
39
+ const mentionedUserIds = [...new Set(payload.mentions ?? [])];
40
+ if (mentionedUserIds.length > 0) {
41
+ const memberRows = await ctx.db.selectMany(tenantMembershipsTable, {
42
+ userId: { in: mentionedUserIds },
43
+ });
44
+ const memberIds = new Set(memberRows.map((row) => row.userId));
45
+ if (mentionedUserIds.some((userId) => !memberIds.has(userId))) {
46
+ return writeFailure(new ValidationError({
47
+ fields: [{ path: "mentions", code: "custom", i18nKey: "errors.validation.custom" }],
48
+ }));
49
+ }
50
+ }
34
51
  let authorName = null;
35
52
  try {
36
53
  // read_users is tenant-agnostic → ctx.db.unsafeRaw, not the tenant-scoped ctx.db.
37
54
  // Bun.SQL poisons the whole tx after any error inside it, even one that's
38
55
  // caught — so the lookup runs in a savepoint where available, otherwise directly
39
56
  // (pool statements are their own units).
40
- authorName = await runInSavepointIfSupported(ctx.db.unsafeRaw(READ_AUTHOR_DISPLAY_NAME_REASON), async (sp) => {
57
+ authorName = await runInSavepointIfSupported(ctx.db.unsafeRaw(), async (sp) => {
41
58
  const userRow = await fetchOne(sp, userTable, {
42
59
  id: event.user.id,
43
60
  });
@@ -54,13 +71,13 @@ export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS) {
54
71
  authorName = null;
55
72
  }
56
73
  const { mentions, ...notePayload } = payload;
57
- const created = await noteEntryExecutor.create({ ...notePayload, authorId: event.user.id, authorName }, event.user, ctx.db);
74
+ const created = await entryExecutor.create({ ...notePayload, authorId: event.user.id, authorName }, event.user, ctx.db);
58
75
  if (!created.isSuccess)
59
76
  return created;
60
77
  // Same tx as the note create above (ctx.db) — a mention-row failure
61
78
  // rolls the note back with it, same as the framework's own nested-write
62
79
  // parent+child pattern (dispatch-write.ts).
63
- for (const subjectId of new Set(mentions ?? [])) {
80
+ for (const subjectId of mentionedUserIds) {
64
81
  const mentionResult = await noteMentionExecutor.create({ noteId: created.data.id, subjectId }, event.user, ctx.db);
65
82
  if (!mentionResult.isSuccess)
66
83
  return mentionResult;
@@ -93,12 +93,30 @@ export const noteEntryDeleteHook = async (ctx) => {
93
93
  });
94
94
  }
95
95
  };
96
- // note-mention rows are a plain (noteId, subjectId) pointer — like authorId
97
- // on note-entry, `subjectId`'s `personal: "ref"` annotation exists for the
98
- // GDPR-hook-coverage boot guard, not because the row itself holds separately
99
- // exportable content or needs its own physical erasure: forgetting the
100
- // mentioned user's data is already handled by noteEntryDeleteHook shredding
101
- // the mentioned NOTE's row-subject key above. Registered here only so the V3
102
- // boot guard (validateGdprPiiHookCoverage) sees a hook for "note-mention".
103
- export const noteMentionExportHook = async () => null;
96
+ // Erasure reaches every note that structurally mentions the user (see
97
+ // noteEntryDeleteHook), so the Art. 15/20 export must list those notes too.
98
+ export const noteMentionExportHook = async (ctx) => {
99
+ const mentions = await ctx.db.selectMany(noteMentionTable, {
100
+ subjectId: ctx.userId,
101
+ });
102
+ if (mentions.length === 0)
103
+ return null;
104
+ const noteIds = [...new Set(mentions.map((m) => m.noteId))];
105
+ const notes = await ctx.db.selectMany(noteEntryTable, { id: { in: noteIds } });
106
+ if (notes.length === 0)
107
+ return null;
108
+ return {
109
+ entity: "note-mention",
110
+ rows: notes.map((note) => ({
111
+ noteId: note["id"],
112
+ entityType: note["entityType"],
113
+ entityId: note["entityId"],
114
+ body: note["body"],
115
+ insertedAt: note["insertedAt"],
116
+ })),
117
+ };
118
+ };
119
+ // note-mention rows are a plain (noteId, subjectId) pointer: forgetting the mentioned
120
+ // user's data is already handled by noteEntryDeleteHook shredding the mentioned NOTE's
121
+ // row-subject key, so this delete hook has nothing of its own to erase.
104
122
  export const noteMentionDeleteHook = async () => { };
@@ -1,4 +1,10 @@
1
1
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { NotificationRenderer } from "../delivery/index.js";
2
3
  import { type RendererContext, type RenderRequest, type RenderResponse } from "../renderer-foundation/index.js";
3
- export declare function adaptToFoundation(req: RenderRequest, ctx: RendererContext): Promise<RenderResponse>;
4
- export declare function createRendererSimpleFeature(): FeatureDefinition;
4
+ import { type MailBranding } from "./simple-renderer.js";
5
+ export declare function adaptToFoundation(req: RenderRequest, ctx: RendererContext, renderer?: NotificationRenderer): Promise<RenderResponse>;
6
+ export type RendererSimpleOptions = {
7
+ /** Branding applied to every notification mail, including the framework auth mails. */
8
+ readonly mailBranding?: MailBranding;
9
+ };
10
+ export declare function createRendererSimpleFeature(options?: RendererSimpleOptions): FeatureDefinition;
@@ -1,7 +1,7 @@
1
1
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { RENDERER_EXTENSION, RendererError, } from "../renderer-foundation/index.js";
3
3
  import { resolveNotificationVariables } from "./resolve-variables.js";
4
- import { simpleRenderer } from "./simple-renderer.js";
4
+ import { createSimpleRenderer, simpleRenderer } from "./simple-renderer.js";
5
5
  // Adapter: simpleRenderer.render hat `Promise<string>`-Signatur (Legacy
6
6
  // NotificationRenderer-Contract), renderer-foundation erwartet
7
7
  // `Promise<RenderResponse>` mit discriminated union. Mapper bewahrt
@@ -9,20 +9,23 @@ import { simpleRenderer } from "./simple-renderer.js";
9
9
  // Inline-CSS) und packt sie in den RendererPlugin-Contract.
10
10
  //
11
11
  // Exported damit der Adapter-Pfad direkt testbar ist (unit-test).
12
- export async function adaptToFoundation(req, ctx) {
12
+ export async function adaptToFoundation(req, ctx, renderer = simpleRenderer) {
13
13
  if (req.kind !== "notification") {
14
14
  // Defensiver Guard — Foundation wählt Plugins nur für matching kinds,
15
15
  // dieser Pfad sollte unter normalen Umständen nie erreicht werden.
16
16
  throw new RendererError(`renderer-simple supports only kind="notification", got "${req.kind}"`, "invalid_payload");
17
17
  }
18
18
  const variables = await resolveNotificationVariables(req, ctx);
19
- const html = await simpleRenderer.render({
19
+ const html = await renderer.render({
20
20
  template: req.payload.template ?? "",
21
21
  variables,
22
22
  });
23
23
  return { kind: "notification", html };
24
24
  }
25
- export function createRendererSimpleFeature() {
25
+ export function createRendererSimpleFeature(options) {
26
+ const renderer = options?.mailBranding
27
+ ? createSimpleRenderer(options.mailBranding)
28
+ : simpleRenderer;
26
29
  return defineFeature("renderer-simple", (r) => {
27
30
  r.describe('Default renderer plugin for `kind="notification"`: takes a structured `EmailTemplateData` variable map (with `header`, `sections[]` of text/button objects, and optional `footer`; falls back to `title`/`body` if no structured fields are present) and returns rendered HTML with inline CSS. Requires `renderer-foundation`; sufficient for plain notification emails \u2014 swap it for `renderer-mail-html` if you need MJML/Markdown layouts.');
28
31
  r.uiHints({
@@ -34,7 +37,7 @@ export function createRendererSimpleFeature() {
34
37
  r.optionalRequires("template-resolver");
35
38
  r.useExtension(RENDERER_EXTENSION, "simple", {
36
39
  kinds: ["notification"],
37
- render: adaptToFoundation,
40
+ render: (req, ctx) => adaptToFoundation(req, ctx, renderer),
38
41
  });
39
42
  });
40
43
  }
@@ -1,2 +1,2 @@
1
- export { createRendererSimpleFeature } from "./feature.js";
2
- export { simpleRenderer } from "./simple-renderer.js";
1
+ export { createRendererSimpleFeature, type RendererSimpleOptions } from "./feature.js";
2
+ export { createSimpleRenderer, type MailBranding, simpleRenderer } from "./simple-renderer.js";
@@ -1,2 +1,2 @@
1
1
  export { createRendererSimpleFeature } from "./feature.js";
2
- export { simpleRenderer } from "./simple-renderer.js";
2
+ export { createSimpleRenderer, simpleRenderer } from "./simple-renderer.js";
@@ -1,2 +1,17 @@
1
1
  import type { NotificationRenderer } from "../delivery/index.js";
2
+ export type MailBranding = {
3
+ /** Shown as text when there is no logo, and as the logo alt text. */
4
+ readonly productName?: string;
5
+ /** Hex color (#rgb, #rrggbb, #rrggbbaa); used for buttons and the header accent. */
6
+ readonly primaryColor?: string;
7
+ /** Absolute http(s) URL; anything else is dropped. */
8
+ readonly logoUrl?: string;
9
+ readonly footerText?: string;
10
+ /** Absolute http(s) URLs only; other entries are dropped. */
11
+ readonly footerLinks?: readonly {
12
+ readonly label: string;
13
+ readonly url: string;
14
+ }[];
15
+ };
16
+ export declare function createSimpleRenderer(branding?: MailBranding): NotificationRenderer;
2
17
  export declare const simpleRenderer: NotificationRenderer;
@@ -1,37 +1,84 @@
1
- import { escapeHtml } from "@cosmicdrift/kumiko-headless";
2
- function renderSection(section) {
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
+ const DEFAULT_PRIMARY_COLOR = "#2563eb";
3
+ const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
4
+ function isHttpUrl(value) {
5
+ try {
6
+ const { protocol } = new URL(value);
7
+ return protocol === "http:" || protocol === "https:";
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ function resolvePrimaryColor(branding) {
14
+ const color = branding?.primaryColor;
15
+ return color && HEX_COLOR_PATTERN.test(color) ? color : DEFAULT_PRIMARY_COLOR;
16
+ }
17
+ function renderBrandingHeader(branding, primaryColor) {
18
+ if (!branding)
19
+ return "";
20
+ const logoUrl = branding.logoUrl && isHttpUrl(branding.logoUrl) ? branding.logoUrl : undefined;
21
+ if (!logoUrl && !branding.productName)
22
+ return "";
23
+ const logo = logoUrl
24
+ ? `<img src="${escapeHtmlAttr(logoUrl)}" alt="${escapeHtmlAttr(branding.productName ?? "")}" style="display:block;max-height:40px;max-width:200px;border:0" />`
25
+ : `<span style="font-size:18px;font-weight:700;color:${escapeHtmlAttr(primaryColor)}">${escapeHtml(branding.productName ?? "")}</span>`;
26
+ return `<div style="margin:0 0 24px;padding:0 0 16px;border-bottom:3px solid ${escapeHtmlAttr(primaryColor)}">${logo}</div>`;
27
+ }
28
+ function renderBrandingFooter(branding) {
29
+ if (!branding)
30
+ return "";
31
+ const links = (branding.footerLinks ?? [])
32
+ .filter((link) => isHttpUrl(link.url))
33
+ .map((link) => `<a href="${escapeHtmlAttr(link.url)}" style="color:#999">${escapeHtml(link.label)}</a>`);
34
+ const footerPartsHtml = [
35
+ branding.footerText ? escapeHtml(branding.footerText) : "",
36
+ links.join(" · "),
37
+ ].filter((part) => part !== "");
38
+ if (footerPartsHtml.length === 0)
39
+ return "";
40
+ const footerHtml = footerPartsHtml.join("<br />");
41
+ return `<p style="margin:16px 0 0;color:#999;font-size:12px">${footerHtml}</p>`;
42
+ }
43
+ function renderSection(section, primaryColor) {
3
44
  if ("text" in section) {
4
45
  return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
5
46
  }
6
47
  if ("button" in section) {
7
- return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:#2563eb;color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
48
+ return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:${escapeHtmlAttr(primaryColor)};color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
8
49
  }
9
50
  return "";
10
51
  }
11
52
  // Simple Renderer: turns structured email template data into HTML with inline CSS.
12
53
  // No external dependencies, no template engine — just string concatenation.
13
- export const simpleRenderer = {
14
- name: "simple",
15
- async render(input) {
16
- const data = input.variables; // @cast-boundary render-helper
17
- // Fallback: if no structured fields, use title + body as header + single text section
18
- const header = data.header ?? data.title;
19
- const sections = data.sections ?? (data.body ? [{ text: data.body }] : undefined);
20
- const parts = [];
21
- parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
22
- parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
23
- if (header) {
24
- parts.push(`<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`);
25
- }
26
- if (sections) {
27
- for (const section of sections) {
28
- parts.push(renderSection(section));
54
+ export function createSimpleRenderer(branding) {
55
+ const primaryColor = resolvePrimaryColor(branding);
56
+ return {
57
+ name: "simple",
58
+ async render(input) {
59
+ const data = input.variables; // @cast-boundary render-helper
60
+ // Fallback: if no structured fields, use title + body as header + single text section
61
+ const header = data.header ?? data.title;
62
+ const sections = data.sections ?? (data.body ? [{ text: data.body }] : undefined);
63
+ const parts = [];
64
+ parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
65
+ parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
66
+ parts.push(renderBrandingHeader(branding, primaryColor));
67
+ if (header) {
68
+ parts.push(`<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`);
69
+ }
70
+ if (sections) {
71
+ for (const section of sections) {
72
+ parts.push(renderSection(section, primaryColor));
73
+ }
29
74
  }
30
- }
31
- if (data.footer) {
32
- parts.push(`<p style="margin:24px 0 0;color:#999;font-size:12px;border-top:1px solid #eee;padding-top:16px">${escapeHtml(data.footer)}</p>`);
33
- }
34
- parts.push("</div></body></html>");
35
- return parts.join("");
36
- },
37
- };
75
+ if (data.footer) {
76
+ parts.push(`<p style="margin:24px 0 0;color:#999;font-size:12px;border-top:1px solid #eee;padding-top:16px">${escapeHtml(data.footer)}</p>`);
77
+ }
78
+ parts.push(renderBrandingFooter(branding));
79
+ parts.push("</div></body></html>");
80
+ return parts.join("");
81
+ },
82
+ };
83
+ }
84
+ export const simpleRenderer = createSimpleRenderer();
@@ -135,7 +135,7 @@ export function createSecretsFeature(opts = {}) {
135
135
  escapeHatch: {
136
136
  reason: ROTATE_ESCAPE_HATCH_REASON,
137
137
  },
138
- handler: (payload, ctx) => rotateJob(payload, ctx, ctx.db.unsafeRaw(ROTATE_ESCAPE_HATCH_REASON)),
138
+ handler: (payload, ctx) => rotateJob(payload, ctx, ctx.db.unsafeRaw()),
139
139
  });
140
140
  // Pre-ES had a separate `retention-cleanup` job scrubbing the audit
141
141
  // table on a compliance-driven schedule (90d default). Post-ES the
@@ -121,7 +121,7 @@ export function createSessionsFeature(options) {
121
121
  name: "cleanup",
122
122
  trigger: { manual: true },
123
123
  escapeHatch: { reason: CLEANUP_ESCAPE_HATCH_REASON },
124
- handler: (payload, ctx) => cleanupJob(payload, ctx, ctx.db.unsafeRaw(CLEANUP_ESCAPE_HATCH_REASON)),
124
+ handler: (payload, ctx) => cleanupJob(payload, ctx, ctx.db.unsafeRaw()),
125
125
  });
126
126
  // Cross-feature entity hook on "user". `r.entityHook` (NOT `r.hook`) is
127
127
  // the supported cross-feature path: entity-keyed, not prefixed by the
@@ -37,10 +37,11 @@ export const revokeAllForUserWrite = defineWriteHandler({
37
37
  description: "Irreversibly signs a named user out of all their live sessions, across every tenant unless a tenantId narrows it, optionally sparing one session (exceptSessionId); use it for operator actions such as freezing or banning an account, or a role change that must not end the editor's own session.",
38
38
  agent: { risk: "high" },
39
39
  escapeHatch: {
40
+ grants: ["unsafeRaw"],
40
41
  reason: REVOKE_ALL_SESSIONS_REASON,
41
42
  },
42
43
  handler: async (event, ctx) => {
43
- const runner = ctx.db.unsafeRaw(REVOKE_ALL_SESSIONS_REASON);
44
+ const runner = ctx.db.unsafeRaw();
44
45
  const updated = await updateMany(runner, userSessionTable, { revokedAt: Temporal.Now.instant() }, {
45
46
  userId: event.payload.userId,
46
47
  revokedAt: null,
@@ -49,6 +49,7 @@ export function createMollieCheckoutSession(client, priceToConfig, appWebhookUrl
49
49
  metadata: {
50
50
  tenantId: options.tenantId,
51
51
  priceId: options.priceId,
52
+ ...(options.consentId && { consentId: options.consentId }),
52
53
  },
53
54
  })); // @cast-boundary engine-bridge
54
55
  const checkoutHref = payment.getCheckoutUrl();
@@ -13,7 +13,7 @@
13
13
  // muss `customerSubscriptions.create` selbst aufrufen). Wir machen
14
14
  // das im Plugin (idempotent via list-check), damit Foundation einen
15
15
  // Created-Event mit subscription-id bekommt.
16
- import { SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
16
+ import { parseProviderConsentId, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
17
17
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
18
18
  import { Temporal } from "temporal-polyfill";
19
19
  import { MOLLIE_PROVIDER_NAME } from "./constants.js";
@@ -85,17 +85,18 @@ export function verifyAndParseMollieWebhook(client, options) {
85
85
  throw new Error(`subscription-mollie: subscription ${subscription.id} has neither nextPaymentDate nor startDate`);
86
86
  }
87
87
  const currentPeriodEnd = mollieDateStringToInstantIso(periodEndSource);
88
+ const consentId = parseProviderConsentId(metadata["consentId"]);
88
89
  return {
89
90
  providerEventId: id,
90
91
  providerName: MOLLIE_PROVIDER_NAME,
91
92
  type,
92
93
  tenantId,
94
+ ...(consentId !== undefined && { consentId }),
93
95
  providerCustomerId: subscription.customerId,
94
96
  providerSubscriptionId: subscription.id,
95
97
  status,
96
98
  tier,
97
99
  currentPeriodEnd,
98
- rawPayload: JSON.stringify({ webhookId: id, subscription, triggerPayment }),
99
100
  };
100
101
  };
101
102
  }
@@ -112,6 +113,7 @@ async function ensureSubscriptionForMandate(client, options, payment) {
112
113
  const paymentMetadata = payment.metadata ?? {}; // @cast-boundary engine-bridge
113
114
  const tenantId = paymentMetadata["tenantId"];
114
115
  const priceId = paymentMetadata["priceId"];
116
+ const consentId = parseProviderConsentId(paymentMetadata["consentId"]);
115
117
  if (!tenantId || !priceId)
116
118
  return null;
117
119
  const priceCfg = options.priceToConfig[priceId];
@@ -130,7 +132,7 @@ async function ensureSubscriptionForMandate(client, options, payment) {
130
132
  amount: { currency: priceCfg.amountCurrency, value: priceCfg.amountValue },
131
133
  interval: priceCfg.interval,
132
134
  description: priceCfg.description,
133
- metadata: { tenantId, priceId },
135
+ metadata: { tenantId, priceId, ...(consentId && { consentId }) },
134
136
  });
135
137
  }
136
138
  // =============================================================================
@@ -8,6 +8,7 @@ export type StripeCheckoutSessionRuntimeOptions = {
8
8
  * Never applied in mode:"subscription" — Stripe rejects invoice_creation there. */
9
9
  readonly paymentInvoiceCreation?: boolean;
10
10
  };
11
+ export declare function mapAppLocaleToStripeLocale(locale: string): Stripe.Checkout.SessionCreateParams.Locale;
11
12
  export declare function createStripeCheckoutSession(runtime: StripeCtxRuntime, { paymentInvoiceCreation }?: StripeCheckoutSessionRuntimeOptions): (ctx: HandlerContext, options: StripeCheckoutOptions) => Promise<{
12
13
  url: string;
13
14
  }>;
@@ -15,7 +16,8 @@ export type StripePortalOptions = Parameters<NonNullable<SubscriptionProviderPlu
15
16
  export declare function createStripePortalSession(runtime: StripeCtxRuntime): (ctx: HandlerContext, options: StripePortalOptions) => Promise<{
16
17
  url: string;
17
18
  }>;
18
- export declare function createStripeCancelSubscription(runtime: StripeCtxRuntime): (ctx: HandlerContext, providerSubscriptionId: string) => Promise<void>;
19
+ export type StripeCancelOptions = Parameters<NonNullable<SubscriptionProviderPlugin["cancelSubscription"]>>[1];
20
+ export declare function createStripeCancelSubscription(runtime: StripeCtxRuntime): (ctx: HandlerContext, options: StripeCancelOptions) => Promise<void>;
19
21
  export type StripeRetrieveSubscriptionOptions = {
20
22
  readonly priceToTier: Readonly<Record<string, string>>;
21
23
  };
@@ -15,10 +15,65 @@
15
15
  // Contract erweitert (z.B. neuer optionaler Field), bemerkt der
16
16
  // Stripe-Plugin das beim TS-Compile, nicht erst zur Laufzeit.
17
17
  import { createHash } from "node:crypto";
18
- import { KNOWN_RECURRING_INTERVALS, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
18
+ import { KNOWN_RECURRING_INTERVALS, SubscriptionCancelTimings, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
19
19
  import { ConflictError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
20
20
  import Stripe from "stripe";
21
21
  import { isResourceMissingStripeError, mapStripeSubscriptionState } from "./verify-webhook.js";
22
+ // Stripe's own Locale union also accepts arbitrary strings, so the known set
23
+ // is spelled out here to pick a value Stripe actually renders.
24
+ const STRIPE_CHECKOUT_LOCALES = [
25
+ "bg",
26
+ "cs",
27
+ "da",
28
+ "de",
29
+ "el",
30
+ "en",
31
+ "en-GB",
32
+ "es",
33
+ "es-419",
34
+ "et",
35
+ "fi",
36
+ "fil",
37
+ "fr",
38
+ "fr-CA",
39
+ "hr",
40
+ "hu",
41
+ "id",
42
+ "it",
43
+ "ja",
44
+ "ko",
45
+ "lt",
46
+ "lv",
47
+ "ms",
48
+ "mt",
49
+ "nb",
50
+ "nl",
51
+ "pl",
52
+ "pt",
53
+ "pt-BR",
54
+ "ro",
55
+ "ru",
56
+ "sk",
57
+ "sl",
58
+ "sv",
59
+ "th",
60
+ "tr",
61
+ "vi",
62
+ "zh",
63
+ "zh-HK",
64
+ "zh-TW",
65
+ ];
66
+ // Stripe rejects custom_text.submit.message above 1200 characters.
67
+ const STRIPE_CUSTOM_TEXT_MAX_LENGTH = 1200;
68
+ export function mapAppLocaleToStripeLocale(locale) {
69
+ const normalized = locale.replace("_", "-");
70
+ const knownLocales = STRIPE_CHECKOUT_LOCALES;
71
+ const exact = knownLocales.find((known) => known.toLowerCase() === normalized.toLowerCase());
72
+ if (exact)
73
+ return exact;
74
+ const language = normalized.split("-")[0]?.toLowerCase();
75
+ return knownLocales.find((known) => known === language) ?? "auto";
76
+ }
22
77
  function isUnknownCustomerStripeError(error) {
23
78
  return (isResourceMissingStripeError(error) &&
24
79
  error instanceof Stripe.errors.StripeInvalidRequestError &&
@@ -34,11 +89,22 @@ export function createStripeCheckoutSession(runtime, { paymentInvoiceCreation =
34
89
  await runtime.assertBillingLive(ctx);
35
90
  const stripe = await runtime.clientForCtx(ctx);
36
91
  const mode = options.mode ?? "subscription";
92
+ const metadata = {
93
+ tenantId: options.tenantId,
94
+ ...(options.consentId && { consentId: options.consentId }),
95
+ };
37
96
  const sessionParams = (providerCustomerId) => ({
38
97
  mode,
39
98
  line_items: [{ price: options.priceId, quantity: 1 }],
40
99
  success_url: options.successUrl,
41
100
  cancel_url: options.cancelUrl,
101
+ submit_type: "pay",
102
+ ...(options.locale && { locale: mapAppLocaleToStripeLocale(options.locale) }),
103
+ ...(options.submitMessage && {
104
+ custom_text: {
105
+ submit: { message: options.submitMessage.slice(0, STRIPE_CUSTOM_TEXT_MAX_LENGTH) },
106
+ },
107
+ }),
42
108
  // subscription_data is subscription-mode-only (Stripe rejects it in
43
109
  // payment-mode) — the subsequent webhook reads metadata.tenantId off
44
110
  // the subscription it creates. payment-mode has no subscription, so
@@ -47,9 +113,9 @@ export function createStripeCheckoutSession(runtime, { paymentInvoiceCreation =
47
113
  // rejects it in subscription-mode, which invoices via the subscription
48
114
  // itself.
49
115
  ...(mode === "subscription"
50
- ? { subscription_data: { metadata: { tenantId: options.tenantId } } }
116
+ ? { subscription_data: { metadata } }
51
117
  : {
52
- payment_intent_data: { metadata: { tenantId: options.tenantId } },
118
+ payment_intent_data: { metadata },
53
119
  invoice_creation: { enabled: paymentInvoiceCreation },
54
120
  // Without a customer, Stripe's default "if_required" makes a guest
55
121
  // checkout when invoice_creation is off; the webhook then has no
@@ -88,17 +154,17 @@ export function createStripePortalSession(runtime) {
88
154
  return { url: session.url };
89
155
  };
90
156
  }
91
- // =============================================================================
92
- // cancelSubscription
93
- // =============================================================================
94
- //
95
- // Stripe sendet danach `customer.subscription.deleted`-webhook → der
96
- // state-update läuft über den normalen webhook-pfad. Diese function
97
- // triggert nur die API-Cancellation.
98
157
  export function createStripeCancelSubscription(runtime) {
99
- return async (ctx, providerSubscriptionId) => {
158
+ return async (ctx, options) => {
100
159
  const stripe = await runtime.clientForCtx(ctx);
101
- await stripe.subscriptions.cancel(providerSubscriptionId);
160
+ if (options.when === SubscriptionCancelTimings.periodEnd) {
161
+ await stripe.subscriptions.update(options.providerSubscriptionId, {
162
+ cancel_at_period_end: true,
163
+ });
164
+ }
165
+ else {
166
+ await stripe.subscriptions.cancel(options.providerSubscriptionId);
167
+ }
102
168
  };
103
169
  }
104
170
  export function createStripeRetrieveSubscription(runtime, { priceToTier }) {
@@ -122,7 +188,6 @@ export function createStripeRetrieveSubscription(runtime, { priceToTier }) {
122
188
  : subscription.customer.id,
123
189
  providerSubscriptionId: subscription.id,
124
190
  ...state,
125
- rawPayload: JSON.stringify(subscription),
126
191
  };
127
192
  };
128
193
  }
@@ -31,7 +31,7 @@
31
31
  // status/tier/period-end mapping. resource_missing (deleted between webhook
32
32
  // and retrieve) returns null; any other Stripe error becomes 503 so Stripe
33
33
  // redelivers.
34
- import { BillingEventKinds, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
34
+ import { BillingEventKinds, parseProviderConsentId, SubscriptionEventTypes, SubscriptionStatuses, } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
35
35
  import { ExtraRouteRejection } from "@cosmicdrift/kumiko-framework/api";
36
36
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
37
37
  import Stripe from "stripe";
@@ -116,15 +116,16 @@ export function verifyAndParseStripeWebhook(runtime, options) {
116
116
  if (!state) {
117
117
  return null;
118
118
  }
119
+ const consentId = parseProviderConsentId(sub.metadata?.["consentId"]);
119
120
  return {
120
121
  providerEventId: event.id,
121
122
  providerName: STRIPE_PROVIDER_NAME,
122
123
  type: normalizedType,
123
124
  tenantId,
125
+ ...(consentId !== undefined && { consentId }),
124
126
  providerCustomerId: typeof sub.customer === "string" ? sub.customer : sub.customer.id,
125
127
  providerSubscriptionId: sub.id,
126
128
  ...state,
127
- rawPayload: JSON.stringify(event),
128
129
  };
129
130
  };
130
131
  }
@@ -269,6 +270,7 @@ function dropPaidSession(event, session, reason) {
269
270
  }
270
271
  /** Parses a checkout.session.completed / .async_payment_succeeded event into
271
272
  * a PaymentEvent, or null if it isn't a paid one-off-payment session. */
273
+ // kumiko-lint-ignore complexity-budget optional-field spreads over the session, one branch per optional provider field
272
274
  async function parsePaymentEvent(event, stripe) {
273
275
  const session = event.data.object; // @cast-boundary engine-bridge
274
276
  // mode !== "payment" excludes subscription-checkout sessions (same event types);
@@ -326,13 +328,15 @@ async function parsePaymentEvent(event, stripe) {
326
328
  if (!providerCustomerId) {
327
329
  return dropPaidSession(event, session, "missing_customer");
328
330
  }
331
+ const consentId = parseProviderConsentId(paymentIntent.metadata?.["consentId"]);
329
332
  return {
330
333
  kind: BillingEventKinds.payment,
331
334
  providerEventId: event.id,
332
335
  providerName: STRIPE_PROVIDER_NAME,
333
336
  tenantId,
337
+ ...(consentId !== undefined && { consentId }),
338
+ providerCheckoutId: session.id,
334
339
  providerCustomerId,
335
340
  priceId,
336
- rawPayload: JSON.stringify(event),
337
341
  };
338
342
  }
@@ -1,2 +1,4 @@
1
+ import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
1
2
  export declare const tagExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor;
2
3
  export declare const tagAssignmentExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor;
4
+ export declare function createTagAssignmentExecutor(ownership: EntityDefinition["access"] | undefined): typeof tagAssignmentExecutor;
@@ -1,7 +1,15 @@
1
1
  import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
2
- import { tagAssignmentEntity, tagEntity } from "./entity.js";
2
+ import { createTagAssignmentEntity, tagAssignmentEntity, tagEntity } from "./entity.js";
3
3
  // Shared executors for the tag + tag-assignment write-handlers.
4
4
  // createEntityExecutor is side-effect-free; instantiating once keeps the
5
5
  // table+executor pair in one place instead of rebuilding it per handler module.
6
6
  export const { executor: tagExecutor } = createEntityExecutor("tag", tagEntity);
7
7
  export const { executor: tagAssignmentExecutor } = createEntityExecutor("tag-assignment", tagAssignmentEntity);
8
+ // Carries only `ownership.write`: the executor enforces entity.access.write on
9
+ // create/restore/delete, while a read rule would hide rows from the handlers'
10
+ // own idempotency reads and the delete-tag cascade.
11
+ export function createTagAssignmentExecutor(ownership) {
12
+ if (ownership?.write === undefined)
13
+ return tagAssignmentExecutor;
14
+ return createEntityExecutor("tag-assignment", createTagAssignmentEntity({ write: ownership.write })).executor;
15
+ }