@cosmicdrift/kumiko-bundled-features 0.363.0 → 0.364.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 (264) hide show
  1. package/dist/audit/handlers/list.query.js +9 -5
  2. package/dist/auth-email-password/bootstrap.d.ts +8 -5
  3. package/dist/auth-email-password/bootstrap.js +12 -4
  4. package/dist/auth-email-password/email-rate-limits.d.ts +4 -0
  5. package/dist/auth-email-password/email-rate-limits.js +13 -0
  6. package/dist/auth-email-password/handlers/change-password.write.d.ts +1 -1
  7. package/dist/auth-email-password/handlers/change-password.write.js +6 -2
  8. package/dist/auth-email-password/handlers/confirm-account-unlock.write.d.ts +1 -1
  9. package/dist/auth-email-password/handlers/invite-accept-with-login.write.d.ts +1 -1
  10. package/dist/auth-email-password/handlers/invite-accept.write.d.ts +1 -1
  11. package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -2
  12. package/dist/auth-email-password/handlers/invite-signup-complete.write.d.ts +1 -1
  13. package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
  14. package/dist/auth-email-password/handlers/logout.write.d.ts +1 -1
  15. package/dist/auth-email-password/handlers/logout.write.js +6 -2
  16. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -1
  17. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -1
  18. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -1
  19. package/dist/auth-email-password/handlers/reset-password.write.d.ts +1 -1
  20. package/dist/auth-email-password/handlers/signup-confirm.write.d.ts +1 -1
  21. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -1
  22. package/dist/auth-email-password/handlers/signup-request.write.js +2 -1
  23. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -1
  24. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
  25. package/dist/auth-email-password/handlers/token-request-handler.js +2 -1
  26. package/dist/auth-email-password/handlers/verify-email.write.d.ts +1 -1
  27. package/dist/auth-email-password/web/auth-gate.js +16 -4
  28. package/dist/auth-mfa/handlers/disable.write.d.ts +1 -1
  29. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.d.ts +1 -1
  30. package/dist/auth-mfa/handlers/enable-confirm.write.d.ts +1 -1
  31. package/dist/auth-mfa/handlers/enable-start-preauth.write.d.ts +1 -1
  32. package/dist/auth-mfa/handlers/enable-start.write.d.ts +1 -1
  33. package/dist/auth-mfa/handlers/regenerate-recovery.write.d.ts +1 -1
  34. package/dist/auth-mfa/handlers/verify.write.d.ts +1 -1
  35. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +52 -5
  36. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -0
  37. package/dist/billing-foundation/consumer-protection/consent-gate.js +21 -2
  38. package/dist/billing-foundation/consumer-protection/mail-notifications.js +1 -1
  39. package/dist/billing-foundation/consumer-protection/register-termination.js +11 -1
  40. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +5 -3
  41. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -2
  42. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +6 -0
  43. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +34 -0
  44. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +2 -0
  45. package/dist/billing-foundation/consumer-protection/termination-record.js +7 -1
  46. package/dist/billing-foundation/consumer-protection/termination-request.js +30 -20
  47. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  48. package/dist/billing-foundation/consumer-protection/termination-texts.js +6 -0
  49. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +10 -0
  50. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +26 -0
  51. package/dist/billing-foundation/handlers/create-checkout-session.write.js +11 -5
  52. package/dist/billing-foundation/handlers/process-event.write.js +10 -6
  53. package/dist/billing-foundation/i18n.js +3 -0
  54. package/dist/billing-foundation/subscription-tier-sync.d.ts +7 -1
  55. package/dist/billing-foundation/subscription-tier-sync.js +13 -5
  56. package/dist/billing-foundation/types.d.ts +7 -2
  57. package/dist/cap-counter/calendar-cap-usage-reservation.js +2 -0
  58. package/dist/cap-counter/enforce-cap.d.ts +18 -1
  59. package/dist/cap-counter/enforce-cap.js +49 -6
  60. package/dist/cap-counter/index.d.ts +1 -1
  61. package/dist/cap-counter/index.js +1 -1
  62. package/dist/cap-counter/with-cap-enforcement.d.ts +2 -0
  63. package/dist/channel-email/email-channel.js +16 -4
  64. package/dist/channel-in-app/handlers/mark-all-read.write.d.ts +1 -1
  65. package/dist/channel-in-app/handlers/mark-read.write.d.ts +1 -1
  66. package/dist/compliance-profiles/handlers/set-profile.write.d.ts +1 -1
  67. package/dist/config/handlers/reset.write.d.ts +1 -1
  68. package/dist/config/handlers/set.write.d.ts +1 -1
  69. package/dist/config/handlers/set.write.js +2 -2
  70. package/dist/config/resolver.d.ts +1 -0
  71. package/dist/config/resolver.js +11 -14
  72. package/dist/config/write-helpers.d.ts +2 -0
  73. package/dist/config/write-helpers.js +19 -0
  74. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
  75. package/dist/crypto-shredding/handlers/forget-subject.write.js +29 -19
  76. package/dist/custom-fields/handlers/update-tenant-field.write.js +0 -1
  77. package/dist/delivery/attachment-ref.d.ts +2 -0
  78. package/dist/delivery/attachment-ref.js +14 -0
  79. package/dist/delivery/channel-context.js +19 -1
  80. package/dist/delivery/handlers/resubscribe-address.write.d.ts +1 -1
  81. package/dist/delivery/handlers/resubscribe-user.write.d.ts +1 -1
  82. package/dist/delivery/handlers/set-preference.write.d.ts +1 -1
  83. package/dist/delivery/handlers/unsubscribe-address.write.d.ts +1 -1
  84. package/dist/delivery/handlers/unsubscribe-user.write.d.ts +1 -1
  85. package/dist/delivery/index.d.ts +1 -1
  86. package/dist/delivery/index.js +1 -1
  87. package/dist/delivery/jobs.js +43 -11
  88. package/dist/delivery/types.d.ts +7 -1
  89. package/dist/feature-toggles/handlers/set.write.d.ts +1 -1
  90. package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
  91. package/dist/file-foundation/read-file-ref-content.d.ts +1 -1
  92. package/dist/file-foundation/read-file-ref-content.js +6 -4
  93. package/dist/files-tenant-data/hooks.js +4 -0
  94. package/dist/form-draft/handlers/discard.write.d.ts +1 -1
  95. package/dist/form-draft/handlers/save.write.d.ts +1 -1
  96. package/dist/inbound-mail-foundation/oauth-access-token.js +8 -3
  97. package/dist/inbound-mail-foundation/watch-supervisor.js +3 -1
  98. package/dist/jobs/db/queries/stale-run-sweep.d.ts +1 -1
  99. package/dist/jobs/db/queries/stale-run-sweep.js +12 -5
  100. package/dist/jobs/feature.js +5 -3
  101. package/dist/jobs/handlers/list.query.js +3 -1
  102. package/dist/jobs/handlers/retry.write.d.ts +1 -1
  103. package/dist/jobs/handlers/stale-run-sweep.job.d.ts +1 -1
  104. package/dist/jobs/handlers/stale-run-sweep.job.js +2 -2
  105. package/dist/jobs/handlers/trigger.write.d.ts +1 -1
  106. package/dist/jobs/job-run-logger.js +32 -10
  107. package/dist/jobs/tenant-job-run-table.js +3 -2
  108. package/dist/ledger/entity.js +3 -1
  109. package/dist/managed-pages/handlers/set.write.d.ts +1 -1
  110. package/dist/personal-access-tokens/handlers/create.write.d.ts +1 -1
  111. package/dist/personal-access-tokens/handlers/revoke.write.d.ts +1 -1
  112. package/dist/renderer-foundation/types.d.ts +2 -0
  113. package/dist/renderer-pdf/render-table-pdf.d.ts +27 -33
  114. package/dist/renderer-pdf/render-table-pdf.js +95 -34
  115. package/dist/secrets/feature.js +4 -3
  116. package/dist/secrets/handlers/delete.write.d.ts +1 -1
  117. package/dist/secrets/handlers/set.write.d.ts +1 -1
  118. package/dist/secrets/secrets-context.js +15 -3
  119. package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -1
  120. package/dist/sessions/handlers/revoke-all-others.write.d.ts +1 -1
  121. package/dist/sessions/handlers/revoke.write.d.ts +1 -1
  122. package/dist/shared/chunked-entity-migration.d.ts +1 -0
  123. package/dist/shared/chunked-entity-migration.js +1 -1
  124. package/dist/shared/mail-template-body.d.ts +3 -0
  125. package/dist/shared/mail-template-body.js +10 -0
  126. package/dist/subscription-stripe/plugin-methods.js +18 -8
  127. package/dist/subscription-stripe/verify-webhook.d.ts +1 -1
  128. package/dist/template-resolver/api.js +3 -2
  129. package/dist/template-resolver/compile-document-body.d.ts +4 -0
  130. package/dist/template-resolver/compile-document-body.js +58 -16
  131. package/dist/template-resolver/constants.d.ts +2 -2
  132. package/dist/template-resolver/constants.js +3 -1
  133. package/dist/template-resolver/document-schema.d.ts +1 -1
  134. package/dist/template-resolver/document-schema.js +23 -3
  135. package/dist/template-resolver/feature.d.ts +2 -0
  136. package/dist/template-resolver/feature.js +4 -1
  137. package/dist/template-resolver/handlers/collection-set.write.d.ts +1 -1
  138. package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
  139. package/dist/template-resolver/handlers/copy-locale.write.js +29 -11
  140. package/dist/template-resolver/handlers/diff-to-system.query.js +4 -38
  141. package/dist/template-resolver/handlers/discard-draft.write.d.ts +13 -0
  142. package/dist/template-resolver/handlers/discard-draft.write.js +37 -0
  143. package/dist/template-resolver/handlers/export-bundle.query.js +51 -1
  144. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  145. package/dist/template-resolver/handlers/find-by-id.query.js +6 -3
  146. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
  147. package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
  148. package/dist/template-resolver/handlers/layout-source.query.d.ts +5 -0
  149. package/dist/template-resolver/handlers/layout-source.query.js +46 -23
  150. package/dist/template-resolver/handlers/list.query.js +4 -1
  151. package/dist/template-resolver/handlers/override-diff.d.ts +20 -0
  152. package/dist/template-resolver/handlers/override-diff.js +41 -0
  153. package/dist/template-resolver/handlers/overview.query.d.ts +6 -0
  154. package/dist/template-resolver/handlers/overview.query.js +51 -11
  155. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +1 -1
  156. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +1 -1
  157. package/dist/template-resolver/handlers/resource-remove.write.d.ts +1 -1
  158. package/dist/template-resolver/handlers/resource-upload.write.d.ts +1 -1
  159. package/dist/template-resolver/handlers/resource-upload.write.js +1 -1
  160. package/dist/template-resolver/handlers/send-test.write.d.ts +3 -1
  161. package/dist/template-resolver/handlers/send-test.write.js +37 -10
  162. package/dist/template-resolver/handlers/set.write.d.ts +1 -1
  163. package/dist/template-resolver/handlers/template-gates.js +13 -2
  164. package/dist/template-resolver/handlers/template-write.js +36 -4
  165. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +32 -0
  166. package/dist/template-resolver/handlers/tenant-overrides.query.js +80 -16
  167. package/dist/template-resolver/handlers/toggle-status.write.d.ts +17 -4
  168. package/dist/template-resolver/handlers/toggle-status.write.js +35 -19
  169. package/dist/template-resolver/handlers/upsert-system.write.d.ts +1 -1
  170. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +1 -1
  171. package/dist/template-resolver/handlers/upsert-tenant.write.js +1 -1
  172. package/dist/template-resolver/handlers/usage.query.js +11 -2
  173. package/dist/template-resolver/index.d.ts +4 -3
  174. package/dist/template-resolver/index.js +4 -3
  175. package/dist/template-resolver/layout-appearance.d.ts +10 -0
  176. package/dist/template-resolver/layout-appearance.js +26 -0
  177. package/dist/template-resolver/pdf-constants.d.ts +7 -0
  178. package/dist/template-resolver/pdf-constants.js +8 -0
  179. package/dist/template-resolver/qualified-names.d.ts +4 -0
  180. package/dist/template-resolver/qualified-names.js +4 -0
  181. package/dist/template-resolver/seeding.js +21 -4
  182. package/dist/template-resolver/store-template-resource.js +2 -2
  183. package/dist/template-resolver/table.d.ts +103 -0
  184. package/dist/template-resolver/table.js +9 -2
  185. package/dist/template-resolver/template-draft.d.ts +51 -0
  186. package/dist/template-resolver/template-draft.js +72 -0
  187. package/dist/template-resolver/template-meta.d.ts +30 -5
  188. package/dist/template-resolver/template-meta.js +60 -16
  189. package/dist/template-resolver/template-preview.d.ts +19 -2
  190. package/dist/template-resolver/template-preview.js +98 -54
  191. package/dist/template-resolver/template-resources.d.ts +5 -0
  192. package/dist/template-resolver/template-resources.js +35 -4
  193. package/dist/template-resolver/template-usage.d.ts +5 -1
  194. package/dist/template-resolver/template-usage.js +14 -9
  195. package/dist/template-resolver/template-validation.js +1 -0
  196. package/dist/template-resolver/web/client-plugin.js +4 -2
  197. package/dist/template-resolver/web/document-editor-extensions.d.ts +4 -1
  198. package/dist/template-resolver/web/document-editor-extensions.js +50 -12
  199. package/dist/template-resolver/web/document-editor.d.ts +2 -1
  200. package/dist/template-resolver/web/document-editor.js +8 -2
  201. package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
  202. package/dist/template-resolver/web/document-tiptap-editor.js +144 -28
  203. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +8 -0
  204. package/dist/template-resolver/web/document-toolbar-blocks.js +4 -0
  205. package/dist/template-resolver/web/i18n.js +19 -4
  206. package/dist/template-resolver/web/index.d.ts +1 -0
  207. package/dist/template-resolver/web/index.js +1 -0
  208. package/dist/template-resolver/web/placeholder-line-editor.d.ts +3 -1
  209. package/dist/template-resolver/web/placeholder-line-editor.js +6 -6
  210. package/dist/tenant/handlers/add-member.write.d.ts +1 -1
  211. package/dist/tenant/handlers/cancel-invitation.write.d.ts +1 -1
  212. package/dist/tenant/handlers/create.write.d.ts +1 -1
  213. package/dist/tenant/handlers/remove-member.write.d.ts +1 -1
  214. package/dist/tenant/handlers/toggle-enabled.write.d.ts +2 -2
  215. package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
  216. package/dist/tenant/handlers/update.write.d.ts +1 -1
  217. package/dist/tenant-handover/handlers/claim.write.d.ts +1 -1
  218. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  219. package/dist/tenant-lifecycle/handlers/request-destruction.write.d.ts +1 -1
  220. package/dist/tier-engine/feature.js +9 -3
  221. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +1 -1
  222. package/dist/user/handlers/create.write.d.ts +1 -1
  223. package/dist/user/handlers/update.write.d.ts +1 -1
  224. package/dist/user/resolve-display-names.js +5 -9
  225. package/dist/user/seeding.js +20 -1
  226. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  227. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.d.ts +1 -1
  228. package/dist/user-data-rights/handlers/lift-restriction.write.d.ts +1 -1
  229. package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +1 -1
  230. package/dist/user-data-rights/handlers/request-deletion.write.d.ts +1 -1
  231. package/dist/user-data-rights/handlers/request-export.write.d.ts +1 -1
  232. package/dist/user-data-rights/handlers/restrict-account.write.d.ts +1 -1
  233. package/dist/user-data-rights/handlers/run-forget-cleanup.write.d.ts +1 -1
  234. package/dist/user-profile/handlers/change-email.write.d.ts +1 -1
  235. package/dist/waitlist/handlers/invite.write.d.ts +1 -1
  236. package/dist/waitlist/handlers/reject.write.d.ts +1 -1
  237. package/dist/waitlist/handlers/submit.write.d.ts +1 -1
  238. package/dist/waitlist/options.js +1 -1
  239. package/dist/workflow-runner/event-trigger.js +2 -2
  240. package/dist/workflow-runner/handlers/resume-run.write.js +7 -2
  241. package/package.json +19 -10
  242. package/src/audit/changes.json +6 -0
  243. package/src/auth-email-password/changes.json +37 -0
  244. package/src/billing-foundation/changes.json +36 -0
  245. package/src/cap-counter/changes.json +6 -0
  246. package/src/config/changes.json +6 -0
  247. package/src/crypto-shredding/changes.json +6 -0
  248. package/src/custom-fields/changes.json +6 -0
  249. package/src/delivery/changes.json +18 -0
  250. package/src/file-foundation/changes.json +6 -0
  251. package/src/files-tenant-data/changes.json +6 -0
  252. package/src/inbound-mail-foundation/changes.json +6 -0
  253. package/src/jobs/changes.json +19 -0
  254. package/src/ledger/changes.json +7 -0
  255. package/src/renderer-pdf/changes.json +12 -0
  256. package/src/secrets/changes.json +6 -0
  257. package/src/shared/changes.json +6 -0
  258. package/src/subscription-stripe/changes.json +6 -0
  259. package/src/template-resolver/changes.json +68 -0
  260. package/src/tier-engine/changes.json +6 -0
  261. package/src/user/changes.json +6 -0
  262. package/src/user-data-rights/changes.json +6 -0
  263. package/src/user-profile/changes.json +1 -1
  264. package/src/workflow-runner/changes.json +6 -0
@@ -17,8 +17,24 @@ export const CAP_TOLERANCES = {
17
17
  export function assertBelowHardCap(valueSeenByLastUnit, cap) {
18
18
  const tolerance = CAP_TOLERANCES[cap.profile];
19
19
  if (valueSeenByLastUnit >= cap.limit * tolerance.hard) {
20
- throw new CapExceededError(cap.capName, cap.limit, valueSeenByLastUnit, tolerance);
20
+ const resetAt = resolvePeriodEndIso(cap.periodStartIso, cap.periodEndIso);
21
+ throw new CapExceededError(cap.capName, cap.limit, valueSeenByLastUnit, tolerance, {
22
+ ...(resetAt !== undefined && { resetAt }),
23
+ ...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
24
+ });
25
+ }
26
+ }
27
+ function resolvePeriodEndIso(periodStartIso, periodEndIso) {
28
+ if (periodStartIso === undefined)
29
+ return periodEndIso;
30
+ if (periodEndIso === undefined)
31
+ return calendarMonthPeriodEndIso(periodStartIso);
32
+ const start = Temporal.Instant.from(periodStartIso);
33
+ const end = Temporal.Instant.from(periodEndIso);
34
+ if (Temporal.Instant.compare(end, start) <= 0) {
35
+ throw new Error(`cap-counter: periodEndIso (${periodEndIso}) must be after periodStartIso (${periodStartIso})`);
21
36
  }
37
+ return end.toString();
22
38
  }
23
39
  // =============================================================================
24
40
  // Enforce-Cap helper
@@ -115,6 +131,9 @@ export async function enforceRollingCap(ctx, options) {
115
131
  * Thrown by enforceCap/enforceRollingCap when value ≥ hard-threshold.
116
132
  * Extends `KumikoError` — the dispatcher auto-maps this to HTTP 429
117
133
  * with `code: "cap_exceeded"`, no caller-side catch needed.
134
+ *
135
+ * Calendar caps carry `resetAt` (end of the period) and `retryAfterSeconds`, which the API
136
+ * answers as Retry-After. Rolling caps have no fixed reset instant, so both stay undefined.
118
137
  */
119
138
  export class CapExceededError extends KumikoError {
120
139
  capName;
@@ -123,17 +142,32 @@ export class CapExceededError extends KumikoError {
123
142
  tolerance;
124
143
  code = "cap_exceeded";
125
144
  httpStatus = 429;
126
- constructor(capName, limit, currentValue, tolerance) {
145
+ resetAt;
146
+ constructor(capName, limit, currentValue, tolerance, options) {
147
+ const resetAt = options?.resetAt;
148
+ const retryAfterSeconds = resetAt === undefined
149
+ ? undefined
150
+ : Math.max(0, Math.ceil(Temporal.Instant.from(resetAt).epochMilliseconds / 1000 -
151
+ (options?.now ?? Temporal.Now.instant()).epochMilliseconds / 1000));
127
152
  super({
128
- message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset.`,
129
- i18nKey: "errors.cap.exceeded",
130
- i18nParams: { capName, limit, currentValue },
131
- details: { capName, limit, currentValue, tolerance },
153
+ message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset.${resetAt === undefined ? "" : ` Resets at ${resetAt}.`}`,
154
+ i18nKey: options?.i18nKey ?? "errors.cap.exceeded",
155
+ i18nParams: { capName, limit, currentValue, ...(resetAt !== undefined && { resetAt }) },
156
+ details: {
157
+ capName,
158
+ limit,
159
+ currentValue,
160
+ tolerance,
161
+ ...(resetAt !== undefined && { resetAt, retryAfterSeconds }),
162
+ },
163
+ ...(retryAfterSeconds !== undefined && { retryAfterSeconds }),
164
+ ...(options?.cause && { cause: options.cause }),
132
165
  });
133
166
  this.capName = capName;
134
167
  this.limit = limit;
135
168
  this.currentValue = currentValue;
136
169
  this.tolerance = tolerance;
170
+ this.resetAt = resetAt;
137
171
  }
138
172
  }
139
173
  // =============================================================================
@@ -158,6 +192,13 @@ export function currentCalendarMonthStartIso(now = Temporal.Now.instant()) {
158
192
  });
159
193
  return start.toInstant().toString();
160
194
  }
195
+ export function calendarMonthPeriodEndIso(periodStartIso) {
196
+ return Temporal.Instant.from(periodStartIso)
197
+ .toZonedDateTimeISO("UTC")
198
+ .add({ months: 1 })
199
+ .toInstant()
200
+ .toString();
201
+ }
161
202
  /**
162
203
  * Calendar-Period-enforcement + automatische soft-hit-Notification.
163
204
  * Ruft `enforceCap`, bei `crossed: true` den Notifier UND
@@ -175,6 +216,8 @@ export async function enforceCapAndMaybeNotify(ctx, options) {
175
216
  limit: options.limit,
176
217
  profile: options.profile,
177
218
  ...(options.amount !== undefined && { amount: options.amount }),
219
+ ...(options.periodEndIso !== undefined && { periodEndIso: options.periodEndIso }),
220
+ ...(options.i18nKey !== undefined && { i18nKey: options.i18nKey }),
178
221
  });
179
222
  if (result.state === "soft-hit" && result.crossed) {
180
223
  await options.notify({
@@ -2,7 +2,7 @@ export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js"
2
2
  export { type BookCapUsageOptions, bookCapUsage, type MarkCapSoftWarnedOptions, markCapSoftWarned, type ReadRollingCapUsageOptions, readRollingCapUsage, } from "./book-cap-usage.js";
3
3
  export { type CalendarCapUsageReservation, type CalendarCapUsageReservationSpec, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
4
4
  export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
5
- export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
5
+ export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
6
6
  export { capCounterEntity } from "./entity.js";
7
7
  export { capCounterFeature } from "./feature.js";
8
8
  export { readPayloadField } from "./payload-field.js";
@@ -3,7 +3,7 @@ export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js"
3
3
  export { bookCapUsage, markCapSoftWarned, readRollingCapUsage, } from "./book-cap-usage.js";
4
4
  export { reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
5
5
  export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
6
- export { CAP_TOLERANCES, CapExceededError, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
6
+ export { CAP_TOLERANCES, CapExceededError, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
7
7
  export { capCounterEntity } from "./entity.js";
8
8
  export { capCounterFeature } from "./feature.js";
9
9
  export { readPayloadField } from "./payload-field.js";
@@ -10,6 +10,8 @@ import { type CapToleranceProfileName, type SoftHitNotifier } from "./enforce-ca
10
10
  export type CalendarCapDef = {
11
11
  readonly capName: string;
12
12
  readonly periodStartIso: string;
13
+ readonly periodEndIso?: string;
14
+ readonly i18nKey?: string;
13
15
  readonly limit: number;
14
16
  readonly profile: CapToleranceProfileName;
15
17
  /** Increment-amount post-success. Default 1. */
@@ -138,13 +138,20 @@ export function createEmailChannel(options) {
138
138
  throw new Error(`email channel: notification "${notificationType}" has a mail template but the channel context carries no renderMail`);
139
139
  }
140
140
  const unsubscribeUrl = message.data?.["unsubscribeUrl"];
141
- const { subject, html, text, attachments } = await ctx.renderMail({
141
+ const { subject, html, text, attachments, fromName, replyTo } = await ctx.renderMail({
142
142
  slug: notificationType,
143
143
  locale: message.locale ?? "en",
144
144
  variables: message.data ?? {},
145
145
  ...(typeof unsubscribeUrl === "string" && { unsubscribeUrl }),
146
146
  });
147
- return { html, subject, text, ...(attachments !== undefined && { attachments }) };
147
+ return {
148
+ html,
149
+ subject,
150
+ text,
151
+ ...(fromName !== undefined && { fromName }),
152
+ ...(replyTo !== undefined && { replyTo }),
153
+ ...(attachments !== undefined && { attachments }),
154
+ };
148
155
  }
149
156
  return {
150
157
  name: EMAIL_CHANNEL_NAME,
@@ -161,8 +168,13 @@ export function createEmailChannel(options) {
161
168
  return renderMessage(message, ctx);
162
169
  },
163
170
  async send(address, message, ctx, rendered) {
164
- const { html, subject, text, attachments } = rendered ?? (await renderMessage(message, ctx));
165
- const envelope = emailEnvelopeFrom(message.data);
171
+ const { html, subject, text, attachments, fromName, replyTo } = rendered ?? (await renderMessage(message, ctx));
172
+ // Template-level sender defaults; notify data wins.
173
+ const envelope = {
174
+ ...(fromName && { fromName }),
175
+ ...(replyTo && { replyTo }),
176
+ ...emailEnvelopeFrom(message.data),
177
+ };
166
178
  const resolvedAttachments = await resolveAttachments([
167
179
  ...(attachments ?? []).map((ref) => ({ ref, origin: "template" })),
168
180
  ...fileAttachmentsFrom(message.data, message.locale).map((ref) => ({ ref, origin: "message" })),
@@ -1,4 +1,4 @@
1
1
  import * as z from "zod";
2
2
  export declare const markAllReadWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"markAllRead", z.ZodObject<{}, z.core.$strip>, {
3
3
  marked: number;
4
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
4
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -3,4 +3,4 @@ export declare const markReadWrite: import("@cosmicdrift/kumiko-types/define-han
3
3
  id: z.ZodNumber;
4
4
  }, z.core.$strip>, {
5
5
  id: number | undefined;
6
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
6
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -10,4 +10,4 @@ export declare const setProfileWrite: import("@cosmicdrift/kumiko-types/define-h
10
10
  }, z.core.$strip>, {
11
11
  profileKey: "de-hr-dsgvo-hgb" | "eu-dsgvo" | "swiss-dsg";
12
12
  isNew: boolean;
13
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
13
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -9,4 +9,4 @@ export declare const resetWrite: import("@cosmicdrift/kumiko-types/define-handle
9
9
  }, z.core.$strip>, {
10
10
  key: string;
11
11
  scope: import("@cosmicdrift/kumiko-types/config-scope").ConfigScope;
12
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
12
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -11,4 +11,4 @@ export declare const setWrite: import("@cosmicdrift/kumiko-types/define-handler"
11
11
  key: string;
12
12
  value: string | number | boolean;
13
13
  scope: import("@cosmicdrift/kumiko-types/config-scope").ConfigScope;
14
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
14
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,6 +1,6 @@
1
1
  import { requestContext } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { configuredPiiSubjectKms, encryptPiiValueForSubject, } from "@cosmicdrift/kumiko-framework/crypto";
3
- import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
+ import { createEventStoreExecutor, scopeTenantDbToTenant } from "@cosmicdrift/kumiko-framework/db";
4
4
  import { ConfigScopes, defineWriteHandler, isEncryptedAtRest, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
5
5
  import { InternalError, isKumikoError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
6
6
  import * as z from "zod";
@@ -74,7 +74,7 @@ export const setWrite = defineWriteHandler({
74
74
  scope,
75
75
  tenantId,
76
76
  userId,
77
- db,
77
+ db: scopeTenantDbToTenant(db, tenantId),
78
78
  });
79
79
  }
80
80
  catch (error) {
@@ -7,6 +7,7 @@ export type ConfigResolverOptions = {
7
7
  cipher?: EnvelopeCipher;
8
8
  appOverrides?: AppConfigOverrides;
9
9
  };
10
+ export declare function decryptStoredValue(raw: string, keyDef: ConfigKeyDefinition, cipher: EnvelopeCipher | undefined, qualifiedKey: string): Promise<string>;
10
11
  export declare function createConfigResolver(options?: ConfigResolverOptions): ConfigResolver;
11
12
  export declare function validateAppOverrides(registry: {
12
13
  getConfigKey: (key: string) => import("@cosmicdrift/kumiko-framework/engine").ConfigKeyDefinition | undefined;
@@ -57,6 +57,15 @@ async function decryptPiiEncrypted(raw, qualifiedKey) {
57
57
  const kmsCtx = { requestId: requestContext.get()?.requestId ?? "config:resolver" };
58
58
  return decryptPiiValueForSubject(kms, raw, kmsCtx, qualifiedKey);
59
59
  }
60
+ // Plaintext form of a stored config_values.value for every read path (resolver,
61
+ // bootstrap idempotency check).
62
+ export async function decryptStoredValue(raw, keyDef, cipher, qualifiedKey) {
63
+ if (keyDef.encrypted)
64
+ return decryptEncrypted(cipher, raw, qualifiedKey);
65
+ if (keyDef.piiEncrypted)
66
+ return decryptPiiEncrypted(raw, qualifiedKey);
67
+ return raw;
68
+ }
60
69
  // backing="secrets" keys store their value in the secrets store (flat per
61
70
  // (tenant,key) at SYSTEM_TENANT_ID), not in config_values. Both read paths
62
71
  // (getWithSource + buildCascade) route the system rung here. Missing reader =
@@ -142,13 +151,7 @@ async function buildCascade(qualifiedKey, keyDef, tenantId, userId, db, fetchRow
142
151
  }
143
152
  const row = await fetchRow(lookup.tenantId, lookup.userId);
144
153
  if (row?.value !== null && row?.value !== undefined) {
145
- let raw = row.value;
146
- if (keyDef.encrypted) {
147
- raw = await decryptEncrypted(cipher, raw, qualifiedKey);
148
- }
149
- else if (keyDef.piiEncrypted) {
150
- raw = await decryptPiiEncrypted(raw, qualifiedKey);
151
- }
154
+ const raw = await decryptStoredValue(row.value, keyDef, cipher, qualifiedKey);
152
155
  if (activeIndex === -1)
153
156
  activeIndex = levels.length;
154
157
  levels.push({
@@ -296,13 +299,7 @@ export function createConfigResolver(options = {}) {
296
299
  for (const lookup of lookups) {
297
300
  const row = await findRow(qualifiedKey, lookup.tenantId, lookup.userId, db);
298
301
  if (row?.value !== null && row?.value !== undefined) {
299
- let raw = row.value;
300
- if (keyDef.encrypted) {
301
- raw = await decryptEncrypted(cipher, raw, qualifiedKey);
302
- }
303
- else if (keyDef.piiEncrypted) {
304
- raw = await decryptPiiEncrypted(raw, qualifiedKey);
305
- }
302
+ const raw = await decryptStoredValue(row.value, keyDef, cipher, qualifiedKey);
306
303
  return { value: deserializeValue(raw, keyDef.type), source: lookup.source };
307
304
  }
308
305
  }
@@ -2,12 +2,14 @@ import type { SubjectId } from "@cosmicdrift/kumiko-framework/crypto";
2
2
  import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { type ConfigKeyDefinition, type ConfigScope, type Registry, type SessionUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { type KumikoError, type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
5
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
5
6
  export type ConfigRowLookup = {
6
7
  readonly id: string;
7
8
  readonly version: number;
8
9
  readonly value: string | null;
9
10
  };
10
11
  export declare function findConfigRow(db: DbConnection | TenantDb, key: string, tenantId: TenantId, userId: string | null): Promise<ConfigRowLookup | null>;
12
+ export declare function configRowHoldsValue(row: ConfigRowLookup | null, keyDef: ConfigKeyDefinition | undefined, cipher: EnvelopeCipher | undefined, qualifiedKey: string, serializedValue: string): Promise<boolean>;
11
13
  export type PrepareConfigWriteArgs = {
12
14
  readonly registry: Registry;
13
15
  readonly user: SessionUser;
@@ -6,6 +6,7 @@ import { ConfigScopes, extensionSelectorTargets, isEncryptedAtRest, SYSTEM_ROLE,
6
6
  import { AccessDeniedError, InternalError, NotFoundError, UnprocessableError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
7
7
  import { assertUnreachable } from "@cosmicdrift/kumiko-framework/utils";
8
8
  import { ConfigErrors } from "./constants.js";
9
+ import { decryptStoredValue } from "./resolver.js";
9
10
  import { configValuesTable } from "./table.js";
10
11
  // Locate an existing config_values row by the (key, tenant, user) triple —
11
12
  // the effective natural key. System-scope rows carry SYSTEM_TENANT_ID on
@@ -26,6 +27,24 @@ export async function findConfigRow(db, key, tenantId, userId) {
26
27
  value: row.value ?? null,
27
28
  };
28
29
  }
30
+ // Idempotency check for declarative writers: does the stored row already hold
31
+ // `serializedValue` (JSON of the plaintext)? Encrypted rows are decrypted
32
+ // first, since their ciphertext differs on every encrypt.
33
+ export async function configRowHoldsValue(row, keyDef, cipher, qualifiedKey, serializedValue) {
34
+ if (row?.value === null || row?.value === undefined)
35
+ return false;
36
+ if (!keyDef?.encrypted && !keyDef?.piiEncrypted)
37
+ return row.value === serializedValue;
38
+ if (!keyDef)
39
+ return false;
40
+ try {
41
+ return (await decryptStoredValue(row.value, keyDef, cipher, qualifiedKey)) === serializedValue;
42
+ }
43
+ catch {
44
+ // Unreadable (rotated key, missing cipher/KMS): the caller rewrites the row, which is the repair.
45
+ return false;
46
+ }
47
+ }
29
48
  export function prepareConfigWrite(args) {
30
49
  const { registry, user, key, scope: requestedScope } = args;
31
50
  const keyDef = registry.getConfigKey(key);
@@ -48,4 +48,4 @@ export declare const forgetSubjectWrite: import("@cosmicdrift/kumiko-types/defin
48
48
  reason: z.ZodString;
49
49
  }, z.core.$strip>, {
50
50
  subjectKey: string;
51
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
51
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,6 +1,6 @@
1
1
  import { requestContext } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
3
- import { runInSavepointIfSupported } from "@cosmicdrift/kumiko-framework/bun-db";
3
+ import { fetchOne, runInSavepointIfSupported, runnerCanBeginTransaction, transaction, } from "@cosmicdrift/kumiko-framework/bun-db";
4
4
  import { computeBlindIndex, configuredBlindIndexKey, configuredPiiSubjectKms, subjectIdSchema, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
5
5
  import { nullBlindIndexesForSubject, recordEventsOwnedExclusivelyByTenant, recordRowExistsInTenant, recordRowOwningTenantId, subjectRowExistsInTenant, } from "@cosmicdrift/kumiko-framework/db";
6
6
  import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
@@ -12,7 +12,7 @@ import * as z from "zod";
12
12
  import { resolveRetentionPolicyForTenant } from "../../data-retention/index.js";
13
13
  import { eraseNoteBodiesReachingSubject } from "../../notes-history-user-data/index.js";
14
14
  import { revokeAllPatTokensForUser } from "../../personal-access-tokens/index.js";
15
- import { USER_STATUS } from "../../user/index.js";
15
+ import { USER_STATUS, userTable } from "../../user/index.js";
16
16
  import { denyIfTargetOutsideAdminTenant, isSystemAdminActor, updateUserLifecycle, } from "../../user-data-rights/index.js";
17
17
  import { CRYPTO_SHREDDING_AGGREGATE_TYPE, CRYPTO_SHREDDING_FEATURE_NAME, RECORD_ENTITY_NOT_REGISTERED, SUBJECT_FORGET_DENIED_EVENT_NAME, SUBJECT_FORGOTTEN_EVENT_NAME, TARGET_RECORD_NOT_ADMIN_TENANT, TARGET_RECORD_RETENTION_BLOCK_DELETE, TARGET_TENANT_NOT_ADMIN_TENANT, } from "../constants.js";
18
18
  export { subjectIdSchema };
@@ -241,22 +241,29 @@ async function sweepDerivedSubjectData(ctx, subject, subjectKey, crossTenantRunn
241
241
  return sweepErrorLabel(err);
242
242
  }
243
243
  }
244
- // Graceful fail: email-subscribers and other non-user entities may use
245
- // user-style subject keys without having an actual user row — the key erase is
246
- // the important part for GDPR compliance; the lifecycle update is best-effort for
247
- // real users. The savepoint confines the failure so the handler transaction
248
- // stays usable for the audit event.
249
- async function closeUserLoginDoorBestEffort(ctx, userId, lifecycleRunner) {
250
- try {
251
- await runInSavepointIfSupported(lifecycleRunner, async (userLifecycleRunner) => {
252
- await updateUserLifecycle(userLifecycleRunner, userId, { status: USER_STATUS.Deleted });
253
- if (ctx.registry.features.has("personal-access-tokens")) {
254
- await revokeAllPatTokensForUser(userLifecycleRunner, userId);
255
- }
256
- });
244
+ // Email subscribers and other non-user entities use user-style subject keys
245
+ // without a read_users row — the key erase is the GDPR-relevant part, so no row
246
+ // means nothing to close. For an existing row, failures propagate: the handler
247
+ // throws and the retry re-runs the (idempotent) erase plus the door close. With
248
+ // an outside-transaction runner that can open its own transaction, the door
249
+ // closes there and stays committed when the handler throws afterwards (sweep
250
+ // error). Otherwise a savepoint confines a failure to the handler transaction.
251
+ async function closeUserLoginDoor(ctx, userId, outsideRunner, handlerTxRunner) {
252
+ const userRow = await fetchOne(handlerTxRunner, userTable, { id: userId });
253
+ // skip: no user row for this subject key (e.g. email subscribers); there is no door to close.
254
+ if (!userRow)
255
+ return;
256
+ const closeDoor = async (runner) => {
257
+ await updateUserLifecycle(runner, userId, { status: USER_STATUS.Deleted });
258
+ if (ctx.registry.features.has("personal-access-tokens")) {
259
+ await revokeAllPatTokensForUser(runner, userId);
260
+ }
261
+ };
262
+ if (outsideRunner && runnerCanBeginTransaction(outsideRunner)) {
263
+ await transaction(outsideRunner, closeDoor);
257
264
  }
258
- catch {
259
- // skip: no user row for this subject key (e.g. email subscribers); key erase and sweeps already ran.
265
+ else {
266
+ await runInSavepointIfSupported(handlerTxRunner, closeDoor);
260
267
  }
261
268
  }
262
269
  // Through the outside-transaction db when available, so the audit event commits
@@ -351,7 +358,6 @@ export const forgetSubjectWrite = defineWriteHandler({
351
358
  userId: event.user.id,
352
359
  eraseReason: event.payload.reason,
353
360
  });
354
- const cleanupError = await sweepDerivedSubjectData(ctx, subject, subjectKey, ctx.db.unsafeRaw(), { requestId, erasedBy: event.user.id });
355
361
  // User subject: close the login door. DEK-erase makes the passwordHash
356
362
  // ciphertext unreadable, but status + PATs are standalone credentials —
357
363
  // the PAT resolver only checks revokedAt/expiresAt, NOT user.status
@@ -367,9 +373,13 @@ export const forgetSubjectWrite = defineWriteHandler({
367
373
  // crash recovery is safe. User-feature guard: without the user feature
368
374
  // read_users doesn't exist (crypto-only stack). Tenant subjects have no
369
375
  // credentials.
376
+ // Runs before the sweep and in its own transaction: a failing sweep rolls the
377
+ // handler transaction back, and the handler tx holds no lock on this user's
378
+ // read_users row yet (only read gates ran), so there is no lock conflict.
370
379
  if (raw.kind === "user" && ctx.registry.features.has("user")) {
371
- await closeUserLoginDoorBestEffort(ctx, raw.userId, ctx.db.unsafeRaw());
380
+ await closeUserLoginDoor(ctx, raw.userId, ctx.dbOutsideTransaction?.unsafeRaw(), ctx.db.unsafeRaw());
372
381
  }
382
+ const cleanupError = await sweepDerivedSubjectData(ctx, subject, subjectKey, ctx.db.unsafeRaw(), { requestId, erasedBy: event.user.id });
373
383
  await appendSubjectForgotten(ctx, event, subjectAggregateId(raw), {
374
384
  subjectKey,
375
385
  reason: event.payload.reason,
@@ -11,7 +11,6 @@ export function createUpdateTenantFieldHandler(opts = {}) {
11
11
  schema: updateFieldPayloadSchema,
12
12
  access: { roles: opts.roles ?? DEFAULT_FIELD_DEFINITION_WRITE_ROLES },
13
13
  description: "Replaces a tenant-owned custom-field definition with a complete new spec; serializedField.type may be omitted, in which case the stored type applies, and a different type is refused. Use it to relabel or re-constrain an existing field rather than to create or remove one.",
14
- excludedFields: ["type"],
15
14
  handler: async (event, ctx) => {
16
15
  const payload = event.payload; // @cast-boundary engine-payload
17
16
  const tenantId = event.user.tenantId;
@@ -4,6 +4,8 @@ export type { FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef }
4
4
  export type EmailAttachmentRef = InlineAttachmentRef | FileAttachmentRef | RenderedPdfAttachmentRef;
5
5
  export declare const MAX_EMAIL_ATTACHMENTS = 20;
6
6
  export declare const MAX_RENDERED_PDF_VARIABLES_BYTES = 64000;
7
+ export declare const DELIVERY_ENVELOPE_DATA_KEYS: readonly ["attachments", "from", "fromName", "replyTo", "headers", "unsubscribeUrl"];
8
+ export declare function withoutDeliveryEnvelopeKeys(variables: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>>;
7
9
  export declare const inlineAttachmentRefSchema: z.ZodObject<{
8
10
  kind: z.ZodLiteral<"inline">;
9
11
  cid: z.ZodString;
@@ -3,6 +3,20 @@ import { attachmentFileNameSchema } from "../shared/index.js";
3
3
  import { isDocumentResourceKey, templateSlugSchema } from "../template-resolver/index.js";
4
4
  export const MAX_EMAIL_ATTACHMENTS = 20;
5
5
  export const MAX_RENDERED_PDF_VARIABLES_BYTES = 64_000;
6
+ // Channel-data keys that steer delivery itself (envelope, headers, attachments);
7
+ // they are not document variables and must not be copied into attached PDF refs.
8
+ export const DELIVERY_ENVELOPE_DATA_KEYS = [
9
+ "attachments",
10
+ "from",
11
+ "fromName",
12
+ "replyTo",
13
+ "headers",
14
+ "unsubscribeUrl",
15
+ ];
16
+ const DELIVERY_ENVELOPE_DATA_KEY_SET = new Set(DELIVERY_ENVELOPE_DATA_KEYS);
17
+ export function withoutDeliveryEnvelopeKeys(variables) {
18
+ return Object.fromEntries(Object.entries(variables).filter(([key]) => !DELIVERY_ENVELOPE_DATA_KEY_SET.has(key)));
19
+ }
6
20
  const MAX_ID_LENGTH = 64;
7
21
  const fileRefIdSchema = z.string().min(1).max(MAX_ID_LENGTH);
8
22
  export const inlineAttachmentRefSchema = z
@@ -1,5 +1,6 @@
1
1
  import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
3
4
  import { readFileRefContent } from "../file-foundation/index.js";
4
5
  import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
5
6
  import { FALLBACK_LOCALE } from "../template-resolver/index.js";
@@ -15,7 +16,10 @@ export function createTemplateMailRenderer(db, registry, tenantId, config) {
15
16
  html: response.html,
16
17
  text: response.text,
17
18
  ...(response.previewText !== undefined && { previewText: response.previewText }),
19
+ ...(response.fromName !== undefined && { fromName: response.fromName }),
20
+ ...(response.replyTo !== undefined && { replyTo: response.replyTo }),
18
21
  ...(response.attachments !== undefined && { attachments: response.attachments }),
22
+ ...(response.layout !== undefined && { layout: response.layout }),
19
23
  };
20
24
  };
21
25
  }
@@ -40,6 +44,19 @@ function createPdfRenderer(db, registry, tenantId, config, filesForTenant) {
40
44
  return { content: response.pdfBytes, contentType: PDF_CONTENT_TYPE };
41
45
  };
42
46
  }
47
+ // A decorative inline image (logo) that cannot be read must not fail the whole mail.
48
+ async function readInlineImageOrNull(fileRefId, read) {
49
+ try {
50
+ return await read();
51
+ }
52
+ catch (err) {
53
+ createFallbackLogger("delivery").warn("inline image could not be read, sending without it", {
54
+ fileRefId,
55
+ errorName: err instanceof Error ? err.name : typeof err,
56
+ });
57
+ return null;
58
+ }
59
+ }
43
60
  // Inline images and file attachments the renderer took from a template may come from the system
44
61
  // tenant (system template or tenant override of one). A file ref from notify data stays limited to
45
62
  // the job's own tenant, since a caller picks it. Inline content that is not an image is dropped, so
@@ -49,12 +66,13 @@ export function createEmailAttachmentReader(db, registry, tenantId, config, file
49
66
  return async (ref, origin = "message") => {
50
67
  if (ref.kind === "rendered-pdf")
51
68
  return renderPdf(ref);
52
- const found = await readFileRefContent({
69
+ const readRef = () => readFileRefContent({
53
70
  db,
54
71
  filesForTenant,
55
72
  fileRefId: ref.fileRefId,
56
73
  allowedTenantIds: ref.kind === "file" && origin === "message" ? [tenantId] : [tenantId, SYSTEM_TENANT_ID],
57
74
  });
75
+ const found = ref.kind === "inline" ? await readInlineImageOrNull(ref.fileRefId, readRef) : await readRef();
58
76
  if (found === null)
59
77
  return null;
60
78
  if (ref.kind === "inline" && !found.contentType.startsWith(INLINE_IMAGE_CONTENT_TYPE_PREFIX)) {
@@ -6,4 +6,4 @@ export declare const resubscribeAddressWrite: import("@cosmicdrift/kumiko-types/
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare const resubscribeUserWrite: import("@cosmicdrift/kumiko-types/def
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -7,4 +7,4 @@ export declare const setPreferenceWrite: import("@cosmicdrift/kumiko-types/defin
7
7
  notificationType: string;
8
8
  channel: string;
9
9
  enabled: boolean;
10
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
10
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare const unsubscribeAddressWrite: import("@cosmicdrift/kumiko-types/
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare const unsubscribeUserWrite: import("@cosmicdrift/kumiko-types/def
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,6 +1,6 @@
1
1
  export { sanitizePdfAttachmentFileName } from "../shared/index.js";
2
2
  export { hashUnsubscribeAddress } from "./address-opt-out.js";
3
- export { type EmailAttachmentRef, emailAttachmentRefSchema, type FileAttachmentRef, fileAttachmentRefSchema, type InlineAttachmentRef, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, type RenderedPdfAttachmentRef, renderedPdfAttachmentRefSchema, toAttachmentRef, } from "./attachment-ref.js";
3
+ export { DELIVERY_ENVELOPE_DATA_KEYS, type EmailAttachmentRef, emailAttachmentRefSchema, type FileAttachmentRef, fileAttachmentRefSchema, type InlineAttachmentRef, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, type RenderedPdfAttachmentRef, renderedPdfAttachmentRefSchema, toAttachmentRef, withoutDeliveryEnvelopeKeys, } from "./attachment-ref.js";
4
4
  export { type AttemptLogRetentionOptions, DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
5
5
  export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
6
6
  export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
@@ -1,6 +1,6 @@
1
1
  export { sanitizePdfAttachmentFileName } from "../shared/index.js";
2
2
  export { hashUnsubscribeAddress } from "./address-opt-out.js";
3
- export { emailAttachmentRefSchema, fileAttachmentRefSchema, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, renderedPdfAttachmentRefSchema, toAttachmentRef, } from "./attachment-ref.js";
3
+ export { DELIVERY_ENVELOPE_DATA_KEYS, emailAttachmentRefSchema, fileAttachmentRefSchema, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, renderedPdfAttachmentRefSchema, toAttachmentRef, withoutDeliveryEnvelopeKeys, } from "./attachment-ref.js";
4
4
  export { DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
5
5
  export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
6
6
  export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";