@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
@@ -30,15 +30,17 @@ export type TerminationDeclaration = {
30
30
  /** The operator notice for a provider problem has no declarant data at all. */
31
31
  export type OperatorNoticeDeclaration = Pick<TerminationDeclaration, "declarationType" | "terminationKind"> & Partial<Omit<TerminationDeclaration, "declarationType" | "terminationKind">>;
32
32
  export declare function formatReceivedAt(iso: string, locale: ConsentLocale): string;
33
+ /** The receipt carries enum fields only: free text from an anonymous caller
34
+ * must not be echoed to an arbitrary address. */
35
+ export type ReceiptDeclaration = Pick<TerminationDeclaration, "declarationType" | "terminationKind">;
33
36
  export type TerminationReceiptArgs = {
34
37
  readonly locale: ConsentLocale;
35
38
  readonly requestId: string;
36
39
  readonly receivedAtIso: string;
37
- readonly declaration: TerminationDeclaration;
40
+ readonly declaration: ReceiptDeclaration;
38
41
  readonly operatorEmail: string;
39
42
  };
40
- /** Built from the declarant's own input only: the content must not depend on
41
- * whether (or how) the declaration matched a contract. */
43
+ /** The content must not depend on whether (or how) the declaration matched a contract. */
42
44
  export declare function terminationReceiptVariables(args: TerminationReceiptArgs): TerminationReceiptVariables;
43
45
  export type OperatorNoticeArgs = {
44
46
  readonly locale: ConsentLocale;
@@ -32,8 +32,7 @@ function declarationLines(declaration, locale) {
32
32
  ];
33
33
  return lines.map((text) => ({ text }));
34
34
  }
35
- /** Built from the declarant's own input only: the content must not depend on
36
- * whether (or how) the declaration matched a contract. */
35
+ /** The content must not depend on whether (or how) the declaration matched a contract. */
37
36
  export function terminationReceiptVariables(args) {
38
37
  const texts = TERMINATION_TEXTS[args.locale];
39
38
  return {
@@ -0,0 +1,6 @@
1
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare const RECEIPT_MAILS_PER_RECIPIENT = 3;
3
+ export declare const RECEIPT_WINDOW_SECONDS: number;
4
+ /** Consumes one receipt token for the recipient. Fails closed when the limiter
5
+ * errors or no digest key is wired: the declaration is already recorded, a missing receipt is the lesser harm. */
6
+ export declare function mayMailReceipt(ctx: HandlerContext, email: string): Promise<boolean>;
@@ -0,0 +1,34 @@
1
+ import { normalizePayloadBucketValue } from "@cosmicdrift/kumiko-framework/pipeline";
2
+ // The declaration itself is a statutory duty and is never refused. Only the
3
+ // receipt mail is capped, so the endpoint cannot be used to mail one inbox
4
+ // from the operator's sender domain.
5
+ export const RECEIPT_MAILS_PER_RECIPIENT = 3;
6
+ export const RECEIPT_WINDOW_SECONDS = 24 * 60 * 60;
7
+ const RECEIPT_BUCKET_PREFIX = "termination-receipt";
8
+ function receiptBucketKey(ctx, email) {
9
+ const digest = ctx.rateLimitDigest?.(normalizePayloadBucketValue(email, "email"));
10
+ return digest === undefined ? undefined : `${RECEIPT_BUCKET_PREFIX}:${digest}`;
11
+ }
12
+ /** Consumes one receipt token for the recipient. Fails closed when the limiter
13
+ * errors or no digest key is wired: the declaration is already recorded, a missing receipt is the lesser harm. */
14
+ export async function mayMailReceipt(ctx, email) {
15
+ // skip: no rate limiter wired, nothing to count against
16
+ if (!ctx.rateLimit)
17
+ return true;
18
+ try {
19
+ const bucketKey = receiptBucketKey(ctx, email);
20
+ if (bucketKey === undefined) {
21
+ ctx.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
22
+ return false;
23
+ }
24
+ const decision = await ctx.rateLimit.check(bucketKey, {
25
+ limit: RECEIPT_MAILS_PER_RECIPIENT,
26
+ windowSeconds: RECEIPT_WINDOW_SECONDS,
27
+ });
28
+ return decision.allowed;
29
+ }
30
+ catch (error) {
31
+ ctx.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
32
+ return false;
33
+ }
34
+ }
@@ -49,6 +49,8 @@ export type RecordContractTerminationResult = {
49
49
  readonly effectiveAtIso: string | null;
50
50
  readonly providerCancel: ProviderCancelOutcome;
51
51
  readonly noticeReason: Extract<OperatorNoticeReason, "provider_cannot_cancel" | "provider_error" | "no_active_subscription"> | null;
52
+ /** An account withdrawal arrived after the period: cancelled at period end instead. */
53
+ readonly withdrawalPeriodExpired: boolean;
52
54
  };
53
55
  export declare const recordUnmatchedContractTerminationSchema: z.ZodObject<{
54
56
  requestId: z.ZodString;
@@ -12,6 +12,7 @@ import { CONTRACT_TERMINATION_CHANNELS, CONTRACT_TERMINATION_DECLARATION_TYPES,
12
12
  import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
13
13
  import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE } from "./consent-locale.js";
14
14
  import { notifyOperator } from "./termination-notify.js";
15
+ import { isWithinWithdrawalPeriod } from "./withdrawal-period.js";
15
16
  const REQUESTED_CANCEL_TIMINGS = [
16
17
  SubscriptionCancelTimings.periodEnd,
17
18
  SubscriptionCancelTimings.immediately,
@@ -148,10 +149,14 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
148
149
  effectiveAtIso: alreadyRecorded.effectiveAtIso,
149
150
  providerCancel: alreadyRecorded.providerCancel,
150
151
  noticeReason: null,
152
+ withdrawalPeriodExpired: false,
151
153
  },
152
154
  };
153
155
  }
154
- const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(ctx, tenantId, payload.when, receivedAtIso);
156
+ const withdrawalPeriodExpired = payload.declarationType === "withdrawal" &&
157
+ payload.when === SubscriptionCancelTimings.immediately &&
158
+ !isWithinWithdrawalPeriod(stream.events, options.now());
159
+ const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(ctx, tenantId, withdrawalPeriodExpired ? SubscriptionCancelTimings.periodEnd : payload.when, receivedAtIso);
155
160
  const recorded = {
156
161
  requestId: payload.requestId,
157
162
  declarationType: payload.declarationType,
@@ -189,6 +194,7 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
189
194
  effectiveAtIso,
190
195
  providerCancel,
191
196
  noticeReason,
197
+ withdrawalPeriodExpired,
192
198
  },
193
199
  };
194
200
  },
@@ -14,7 +14,7 @@ import { purchaseRolesOf } from "../plan-catalog.js";
14
14
  import { resolveConsentLocale } from "./consent-text.js";
15
15
  import { terminationReceiptVariables } from "./termination-mail.js";
16
16
  import { notifyOperator, requireNotify } from "./termination-notify.js";
17
- const TENANT_ADMIN_ROLE = "TenantAdmin";
17
+ import { mayMailReceipt } from "./termination-receipt-limit.js";
18
18
  const EXTRAORDINARY_REASON_MESSAGE = "reason is required for an extraordinary termination";
19
19
  const authUserRowSchema = z.object({
20
20
  id: z.string().min(1),
@@ -53,27 +53,36 @@ function needsOperatorNotice(declaration) {
53
53
  ];
54
54
  }
55
55
  async function sendTerminationMails(ctx, consumerProtection, plan) {
56
- await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
57
- route: { email: plan.declaration.email },
58
- data: terminationReceiptVariables({
56
+ const receiptAllowed = await mayMailReceipt(ctx, plan.declaration.email);
57
+ if (receiptAllowed) {
58
+ await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
59
+ route: { email: plan.declaration.email },
60
+ data: terminationReceiptVariables({
61
+ locale: plan.locale,
62
+ requestId: plan.requestId,
63
+ receivedAtIso: plan.receivedAtIso,
64
+ declaration: {
65
+ declarationType: plan.declaration.declarationType,
66
+ terminationKind: plan.declaration.terminationKind,
67
+ },
68
+ operatorEmail: consumerProtection.operatorEmail,
69
+ }),
59
70
  locale: plan.locale,
60
- requestId: plan.requestId,
61
- receivedAtIso: plan.receivedAtIso,
62
- declaration: plan.declaration,
63
- operatorEmail: consumerProtection.operatorEmail,
64
- }),
65
- locale: plan.locale,
66
- priority: "critical",
67
- });
71
+ priority: "critical",
72
+ });
73
+ }
74
+ const reasons = receiptAllowed
75
+ ? plan.reasons
76
+ : [...plan.reasons, "receipt_rate_limited"];
68
77
  // skip: no operator notice needed for this declaration
69
- if (plan.reasons.length === 0)
78
+ if (reasons.length === 0)
70
79
  return;
71
- await notifyOperator(ctx, consumerProtection, plan);
80
+ await notifyOperator(ctx, consumerProtection, { ...plan, reasons });
72
81
  }
73
82
  // Tenants the entered email can terminate a contract for: tenants where the
74
- // user is TenantAdmin and the subscription is not terminal. Every identity
83
+ // user holds a purchase role and the subscription is not terminal. Every identity
75
84
  // below comes from the trusted lookups, never from the payload.
76
- async function findTerminableTenantIds(ctx, requestTenantId, email) {
85
+ async function findTerminableTenantIds(ctx, options, requestTenantId, email) {
77
86
  declareEscapeHatch({
78
87
  reason: "anonymous declarant has no session — finds the contract by email via user:find-for-auth and tenant:memberships as a system user; the calling handler declares its own escapeHatch",
79
88
  });
@@ -85,9 +94,10 @@ async function findTerminableTenantIds(ctx, requestTenantId, email) {
85
94
  }));
86
95
  if (!memberships.success)
87
96
  return [];
97
+ const purchaseRoles = purchaseRolesOf(options.catalog);
88
98
  const adminTenantIds = [
89
99
  ...new Set(memberships.data
90
- .filter((membership) => membership.roles.includes(TENANT_ADMIN_ROLE))
100
+ .filter((membership) => membership.roles.some((role) => purchaseRoles.includes(role)))
91
101
  .map((membership) => membership.tenantId)),
92
102
  ];
93
103
  const terminable = [];
@@ -105,7 +115,6 @@ export function createRequestContractTerminationHandler(options, consumerProtect
105
115
  schema: requestContractTerminationSchema,
106
116
  access: { roles: ["anonymous"] },
107
117
  rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
108
- additionalRateLimits: [{ per: { payloadField: "email" }, limit: 3, windowSeconds: 86400 }],
109
118
  ...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
110
119
  escapeHatch: {
111
120
  reason: "Anonymous declarant has no session — finds the contract by email via ctx.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
@@ -119,7 +128,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
119
128
  const reasons = [];
120
129
  const locale = resolveConsentLocale(payload.locale);
121
130
  let matchedTenantId;
122
- const candidates = await findTerminableTenantIds(ctx, event.user.tenantId, payload.email);
131
+ const candidates = await findTerminableTenantIds(ctx, options, event.user.tenantId, payload.email);
123
132
  const [onlyCandidate] = candidates;
124
133
  if (candidates.length === 1 && onlyCandidate !== undefined) {
125
134
  matchedTenantId = onlyCandidate;
@@ -168,7 +177,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
168
177
  export function createTerminateContractHandler(options, consumerProtection) {
169
178
  return {
170
179
  name: "terminate-contract",
171
- description: "Terminates the tenant's subscription at the end of the billing period, or withdraws from the contract and cancels it immediately; sends a receipt to the caller's email.",
180
+ description: "Terminates the tenant's subscription at the end of the billing period, or withdraws from the contract and cancels it immediately within the 14-day withdrawal period (at period end after it); sends a receipt to the caller's email.",
172
181
  schema: terminateContractSchema,
173
182
  access: { roles: purchaseRolesOf(options.catalog) },
174
183
  escapeHatch: {
@@ -214,6 +223,7 @@ export function createTerminateContractHandler(options, consumerProtection) {
214
223
  },
215
224
  reasons: [
216
225
  ...(data.noticeReason !== null ? [data.noticeReason] : []),
226
+ ...(data.withdrawalPeriodExpired ? ["withdrawal_period_expired"] : []),
217
227
  ...needsOperatorNotice(payload),
218
228
  ],
219
229
  tenantId,
@@ -58,6 +58,6 @@ export type PageTexts = {
58
58
  readonly rateLimitedTitle: string;
59
59
  readonly rateLimitedBody: string;
60
60
  };
61
- export declare const OPERATOR_NOTICE_REASONS: readonly ["unmatched", "ambiguous", "withdrawal", "extraordinary", "provider_cannot_cancel", "provider_error", "no_active_subscription", "recording_failed"];
61
+ export declare const OPERATOR_NOTICE_REASONS: readonly ["unmatched", "ambiguous", "withdrawal", "extraordinary", "provider_cannot_cancel", "provider_error", "no_active_subscription", "recording_failed", "withdrawal_period_expired", "receipt_rate_limited"];
62
62
  export type OperatorNoticeReason = (typeof OPERATOR_NOTICE_REASONS)[number];
63
63
  export declare const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>>;
@@ -7,6 +7,8 @@ export const OPERATOR_NOTICE_REASONS = [
7
7
  "provider_error",
8
8
  "no_active_subscription",
9
9
  "recording_failed",
10
+ "withdrawal_period_expired",
11
+ "receipt_rate_limited",
10
12
  ];
11
13
  export const TERMINATION_TEXTS = {
12
14
  de: {
@@ -46,6 +48,8 @@ export const TERMINATION_TEXTS = {
46
48
  provider_error: "Die Kündigung beim Zahlungsanbieter ist fehlgeschlagen",
47
49
  no_active_subscription: "Keine laufende Subscription beim Anbieter",
48
50
  recording_failed: "Die Erklärung konnte nicht gespeichert werden",
51
+ withdrawal_period_expired: "Widerruf nach Ablauf der Widerrufsfrist: zum Periodenende gekündigt, Anspruch prüfen",
52
+ receipt_rate_limited: "Keine Eingangsbestätigung versendet: Limit pro Empfängeradresse erreicht",
49
53
  },
50
54
  noticeReasons: "Grund der Benachrichtigung",
51
55
  channel: "Kanal",
@@ -125,6 +129,8 @@ export const TERMINATION_TEXTS = {
125
129
  provider_error: "Cancelling at the payment provider failed",
126
130
  no_active_subscription: "No running subscription at the provider",
127
131
  recording_failed: "The declaration could not be stored",
132
+ withdrawal_period_expired: "Withdrawal after the withdrawal period: cancelled at period end, review the claim",
133
+ receipt_rate_limited: "No receipt mail sent: the per-recipient limit was reached",
128
134
  },
129
135
  noticeReasons: "Reason for this notice",
130
136
  channel: "Channel",
@@ -0,0 +1,10 @@
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ export declare const WITHDRAWAL_PERIOD_DAYS = 14;
3
+ type PeriodStartEvent = {
4
+ readonly type: string;
5
+ readonly createdAt: Temporal.Instant;
6
+ };
7
+ /** The period runs from the latest consent or subscription start on the stream.
8
+ * A stream with neither has no provable start and counts as expired. */
9
+ export declare function isWithinWithdrawalPeriod(events: readonly PeriodStartEvent[], now: Temporal.Instant): boolean;
10
+ export {};
@@ -0,0 +1,26 @@
1
+ import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN } from "../events.js";
2
+ // § 355 BGB: the withdrawal period is 14 days from conclusion of the contract.
3
+ export const WITHDRAWAL_PERIOD_DAYS = 14;
4
+ const HOURS_PER_DAY = 24;
5
+ const PERIOD_START_EVENT_TYPES = new Set([
6
+ CHECKOUT_CONSENT_RECORDED_EVENT_QN,
7
+ SUBSCRIPTION_CREATED_EVENT_QN,
8
+ ]);
9
+ /** The period runs from the latest consent or subscription start on the stream.
10
+ * A stream with neither has no provable start and counts as expired. */
11
+ export function isWithinWithdrawalPeriod(events, now) {
12
+ let latestStart;
13
+ for (const event of events) {
14
+ if (!PERIOD_START_EVENT_TYPES.has(event.type))
15
+ continue;
16
+ if (latestStart === undefined ||
17
+ event.createdAt.epochMilliseconds > latestStart.epochMilliseconds) {
18
+ latestStart = event.createdAt;
19
+ }
20
+ }
21
+ // skip: no consent or subscription-created event, nothing proves a running period
22
+ if (latestStart === undefined)
23
+ return false;
24
+ const periodEnd = latestStart.add({ hours: WITHDRAWAL_PERIOD_DAYS * HOURS_PER_DAY });
25
+ return now.epochMilliseconds <= periodEnd.epochMilliseconds;
26
+ }
@@ -21,10 +21,10 @@
21
21
  // billing-foundation:write:switch-plan instead. Plan-driven callers should
22
22
  // prefer start-plan-checkout/switch-plan over calling this handler with a
23
23
  // hand-picked priceId.
24
- import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
24
+ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
25
25
  import * as z from "zod";
26
26
  import { assertCheckoutAllowed, openCheckout, startProviderCheckout, } from "../checkout-core.js";
27
- import { consentCheckoutFields, consentPayloadSchema, prepareConsent, recordConsent, } from "../consumer-protection/consent-gate.js";
27
+ import { assertValidOrderItem, consentCheckoutFields, consentPayloadSchema, prepareConsent, recordConsent, } from "../consumer-protection/consent-gate.js";
28
28
  const createCheckoutSessionSchema = z.object({
29
29
  /** Welcher Provider — entityName eines registrierten subscription-
30
30
  * Plugins ("stripe" / "paypal" / ...). */
@@ -75,10 +75,11 @@ export function createCheckoutSessionHandler(options) {
75
75
  }
76
76
  const mode = payload.mode ?? "subscription";
77
77
  const plugin = await assertCheckoutAllowed(ctx, openOptions, checkoutInput);
78
- const price = await lookupPrice(ctx, plugin, payload.priceId);
78
+ const price = await lookupPrice(ctx, plugin, payload.providerName, payload.priceId);
79
79
  const orderItem = mode === "payment"
80
80
  ? await consumerProtection.oneOffItemLabel?.(ctx, payload.priceId)
81
81
  : undefined;
82
+ assertValidOrderItem(orderItem, payload.priceId);
82
83
  const consent = await prepareConsent(ctx, consumerProtection, payload.consent, mode);
83
84
  const result = await startProviderCheckout(ctx, plugin, {
84
85
  ...checkoutInput,
@@ -99,10 +100,15 @@ export function createCheckoutSessionHandler(options) {
99
100
  }
100
101
  // The price is part of the consent record, so it must be known before the
101
102
  // buyer is sent to the provider.
102
- async function lookupPrice(ctx, plugin, priceId) {
103
+ async function lookupPrice(ctx, plugin, providerName, priceId) {
104
+ if (!plugin.retrievePrices) {
105
+ throw new InternalError({
106
+ message: `billing-foundation: provider "${providerName}" lacks retrievePrices, required by consumerProtection`,
107
+ });
108
+ }
103
109
  let price;
104
110
  try {
105
- const prices = (await plugin.retrievePrices?.(ctx, [priceId])) ?? [];
111
+ const prices = await plugin.retrievePrices(ctx, [priceId]);
106
112
  price = prices.find((p) => p.priceId === priceId);
107
113
  }
108
114
  catch (cause) {
@@ -14,7 +14,7 @@
14
14
  import { configuredPiiSubjectKms, encryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
15
15
  import * as z from "zod";
16
16
  import { subscriptionAggregateId } from "../aggregate-id.js";
17
- import { isSubscriptionBlockingCheckout, SubscriptionEventTypes, SubscriptionStatuses, } from "../constants.js";
17
+ import { isSubscriptionBlockingCheckout, isSwitchableSubscriptionStatus, SubscriptionEventTypes, SubscriptionStatuses, } from "../constants.js";
18
18
  import { SUBSCRIPTION_PII_FIELDS, subscriptionEntity } from "../entities.js";
19
19
  import { INVOICE_PAID_EVENT_QN, INVOICE_PAYMENT_FAILED_EVENT_QN, SUBSCRIPTION_AGGREGATE_TYPE, SUBSCRIPTION_CANCELED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_QN, } from "../events.js";
20
20
  import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
@@ -154,16 +154,20 @@ export async function appendSubscriptionEvent(ctx, tenantId, payload) {
154
154
  // Tenant-Admin direkt.
155
155
  // One subscription aggregate exists per tenant, but a tenant can legitimately hold two provider
156
156
  // subscriptions over time (a stale `incomplete` one superseded by a fresh checkout). A late or
157
- // retried event of the old subscription must not overwrite the row of the live one, so only a
158
- // `created` event may replace a blocking row that belongs to a different provider subscription.
157
+ // retried event of the old subscription must not overwrite the row of the live one, so an event of a
158
+ // different provider subscription only replaces a row that no longer blocks a checkout. The exception
159
+ // is a `created` of a subscription that is itself live (a provider switch); a late `created` of a
160
+ // never-completed `incomplete` one stays stale, since delivery order is not guaranteed.
159
161
  function isStaleSubscriptionEvent(current, payload, now) {
160
162
  if (current === null)
161
163
  return false;
162
- if (payload.type === SubscriptionEventTypes.created)
163
- return false;
164
164
  const sameSubscription = current.providerName === payload.providerName &&
165
165
  current.providerSubscriptionId === payload.providerSubscriptionId;
166
- return !sameSubscription && isSubscriptionBlockingCheckout(current, now);
166
+ if (sameSubscription || !isSubscriptionBlockingCheckout(current, now))
167
+ return false;
168
+ const supersedesCurrent = payload.type === SubscriptionEventTypes.created &&
169
+ isSwitchableSubscriptionStatus(payload.status);
170
+ return !supersedesCurrent;
167
171
  }
168
172
  export function createProcessEventHandler(now) {
169
173
  return {
@@ -90,6 +90,9 @@ export const BILLING_FOUNDATION_I18N = {
90
90
  "billing-foundation.errors.redirectOriginNotAllowed": {
91
91
  en: "This redirect URL is not allowed for this app.",
92
92
  },
93
+ "billing-foundation.errors.providerCustomerUnknown": {
94
+ en: "The billing account for this subscription no longer exists. Subscribe again to continue.",
95
+ },
93
96
  "billing-foundation.errors.providerHasNoPriceCatalog": {
94
97
  en: "This provider has no known prices configured.",
95
98
  },
@@ -1,11 +1,17 @@
1
- import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
1
+ import type { ExtraRouteDefinition, SignatureExtraRouteDeps } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
3
  export declare const SUBSCRIPTION_WEBHOOK_PATH = "/webhooks/subscription/:providerName";
3
4
  export type SubscriptionTierSyncDeps<TTier extends string> = {
4
5
  readonly isTierName: (value: string) => value is TTier;
5
6
  readonly defaultTier: TTier;
6
7
  readonly onSyncError?: "log" | "fail-webhook";
7
8
  };
9
+ export declare const TIER_SYNC_MANUAL_GRANT_SKIPPED = "tier_sync_manual_grant_skipped";
8
10
  export declare function effectiveTierFromSubscription<TTier extends string>(status: string | undefined, tier: string | undefined, isTierName: (value: string) => value is TTier, defaultTier: TTier): TTier;
9
11
  export declare function createSubscriptionTierSync<TTier extends string>(deps: SubscriptionTierSyncDeps<TTier>): {
10
12
  createWebhookRoute: () => ExtraRouteDefinition;
13
+ syncTierFromSubscription: (tenantId: TenantId, routeDeps: Pick<SignatureExtraRouteDeps, "dispatchSystemQuery" | "dispatchSystemWrite">) => Promise<{
14
+ code: string;
15
+ message: string;
16
+ } | null>;
11
17
  };
@@ -10,6 +10,9 @@ import { createSubscriptionWebhookRoute } from "./webhook-handler.js";
10
10
  // Outside /api — signature routes carry their own auth (verify()) and are
11
11
  // not JWT-guarded, so provider dashboards (Stripe, PayPal, ...) point here.
12
12
  export const SUBSCRIPTION_WEBHOOK_PATH = "/webhooks/subscription/:providerName";
13
+ // A manual grant that differs from what billing would assign is reported with this
14
+ // code but never fails the webhook: a provider retry cannot resolve it.
15
+ export const TIER_SYNC_MANUAL_GRANT_SKIPPED = "tier_sync_manual_grant_skipped";
13
16
  export function effectiveTierFromSubscription(status, tier, isTierName, defaultTier) {
14
17
  const usable = status === SubscriptionStatuses.active || status === SubscriptionStatuses.trialing;
15
18
  return usable && typeof tier === "string" && isTierName(tier) ? tier : defaultTier;
@@ -68,10 +71,14 @@ export function createSubscriptionTierSync(deps) {
68
71
  }
69
72
  return null;
70
73
  }
71
- if (assignment.source === TierAssignmentSources.manual)
72
- return null;
73
74
  if (assignment.tier === effective)
74
75
  return null;
76
+ if (assignment.source === TierAssignmentSources.manual) {
77
+ return {
78
+ code: TIER_SYNC_MANUAL_GRANT_SKIPPED,
79
+ message: `manual tier grant "${String(assignment.tier)}" kept although the subscription yields "${effective}"`,
80
+ };
81
+ }
75
82
  const result = await routeDeps.dispatchSystemWrite({
76
83
  handlerQn: TierEngineHandlers.update,
77
84
  payload: {
@@ -108,8 +115,9 @@ export function createSubscriptionTierSync(deps) {
108
115
  const syncError = await syncTierFromSubscription(tenantId, routeDeps);
109
116
  if (syncError) {
110
117
  // biome-ignore lint/suspicious/noConsole: operator visibility for a post-commit sync failure
111
- console.warn(`[subscription-tier-sync] tier sync failed for tenant ${tenantId} after successful webhook write: ${syncError.code} ${syncError.message}`);
112
- if (deps.onSyncError === "fail-webhook") {
118
+ console.warn(`[subscription-tier-sync] tier sync for tenant ${tenantId} after successful webhook write: ${syncError.code} ${syncError.message}`);
119
+ if (deps.onSyncError === "fail-webhook" &&
120
+ syncError.code !== TIER_SYNC_MANUAL_GRANT_SKIPPED) {
113
121
  return {
114
122
  isSuccess: false,
115
123
  error: {
@@ -125,5 +133,5 @@ export function createSubscriptionTierSync(deps) {
125
133
  },
126
134
  });
127
135
  }
128
- return { createWebhookRoute };
136
+ return { createWebhookRoute, syncTierFromSubscription };
129
137
  }
@@ -289,7 +289,10 @@ export type LegalLinkSet = {
289
289
  export declare function isLegalLinkSet(links: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>): links is LegalLinkSet;
290
290
  /** Consumer-protection consent gate (§ 312j / § 356 BGB style). When set,
291
291
  * `start-plan-checkout` and `create-checkout-session` require a `consent`
292
- * payload and record it as an event before the buyer reaches the provider. */
292
+ * payload and record it as an event before the buyer reaches the provider.
293
+ * Every provider used with `create-checkout-session` must implement
294
+ * `retrievePrices`: the live price is part of the consent record, and a
295
+ * provider without it fails the checkout with an InternalError. */
293
296
  export type ConsumerProtectionOptions = {
294
297
  /** Slug of the template-resolver `text-block` holding the terms text; its
295
298
  * content hash is stored with every recorded consent. */
@@ -304,7 +307,9 @@ export type ConsumerProtectionOptions = {
304
307
  readonly terminationScope?: TerminationScope;
305
308
  /** Names the item of a one-off payment (mode "payment") for the consent record
306
309
  * and the confirmation mail; the label key is resolved through the registry
307
- * i18n, so it must be qualified with the registering feature. */
310
+ * i18n, so it must be qualified with the registering feature. The label key
311
+ * is 1-200 characters, string params at most 100; a violation fails the
312
+ * checkout with an InternalError before the provider session is created. */
308
313
  readonly oneOffItemLabel?: (ctx: HandlerContext, priceId: string) => Promise<OrderItem | undefined>;
309
314
  /** Root-relative paths or absolute https URLs. One set for every locale, or
310
315
  * one set per consent locale (`de` and `en` required). */
@@ -12,6 +12,8 @@ export async function reserveEnforcedCalendarCap(ctx, cap) {
12
12
  profile: cap.profile,
13
13
  notify: cap.notify,
14
14
  amount: cap.amount,
15
+ ...(cap.periodEndIso !== undefined && { periodEndIso: cap.periodEndIso }),
16
+ ...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
15
17
  markSoftWarnedOutsideTransaction: outsideTransaction,
16
18
  });
17
19
  }
@@ -57,6 +57,9 @@ export declare function assertBelowHardCap(valueSeenByLastUnit: number, cap: {
57
57
  readonly capName: string;
58
58
  readonly limit: number;
59
59
  readonly profile: CapToleranceProfileName;
60
+ readonly periodStartIso?: string;
61
+ readonly periodEndIso?: string;
62
+ readonly i18nKey?: string;
60
63
  }): void;
61
64
  /**
62
65
  * Synchronous read-and-check of the calling tenant's counter for
@@ -85,6 +88,8 @@ export declare function enforceCap(ctx: CapCounterContext, options: {
85
88
  readonly limit: number;
86
89
  readonly profile: CapToleranceProfileName;
87
90
  readonly amount?: number;
91
+ readonly periodEndIso?: string;
92
+ readonly i18nKey?: string;
88
93
  }): Promise<EnforceCapResult>;
89
94
  /**
90
95
  * Synchronous read-and-check of the calling tenant's Rolling-Window-
@@ -120,6 +125,9 @@ export declare function enforceRollingCap(ctx: CapCounterContext, options: {
120
125
  * Thrown by enforceCap/enforceRollingCap when value ≥ hard-threshold.
121
126
  * Extends `KumikoError` — the dispatcher auto-maps this to HTTP 429
122
127
  * with `code: "cap_exceeded"`, no caller-side catch needed.
128
+ *
129
+ * Calendar caps carry `resetAt` (end of the period) and `retryAfterSeconds`, which the API
130
+ * answers as Retry-After. Rolling caps have no fixed reset instant, so both stay undefined.
123
131
  */
124
132
  export declare class CapExceededError extends KumikoError {
125
133
  readonly capName: string;
@@ -128,7 +136,13 @@ export declare class CapExceededError extends KumikoError {
128
136
  readonly tolerance: CapToleranceProfile;
129
137
  readonly code: "cap_exceeded";
130
138
  readonly httpStatus = 429;
131
- constructor(capName: string, limit: number, currentValue: number, tolerance: CapToleranceProfile);
139
+ readonly resetAt: string | undefined;
140
+ constructor(capName: string, limit: number, currentValue: number, tolerance: CapToleranceProfile, options?: {
141
+ readonly resetAt?: string;
142
+ readonly i18nKey?: string;
143
+ readonly cause?: Error;
144
+ readonly now?: Temporal.Instant;
145
+ });
132
146
  }
133
147
  /**
134
148
  * Calendar-month period start in UTC. Use this for monthly caps
@@ -137,6 +151,7 @@ export declare class CapExceededError extends KumikoError {
137
151
  * Returns ISO string for the 1st of the current month at 00:00 UTC.
138
152
  */
139
153
  export declare function currentCalendarMonthStartIso(now?: Temporal.Instant): string;
154
+ export declare function calendarMonthPeriodEndIso(periodStartIso: string): string;
140
155
  /**
141
156
  * Soft-hit-notifier callback. Caller liefert die Funktion die ein
142
157
  * delivery-event emittet (z.B. `delivery.send({to, template, payload})`).
@@ -169,6 +184,8 @@ export declare function enforceCapAndMaybeNotify(ctx: CapCounterContext, options
169
184
  readonly profile: CapToleranceProfileName;
170
185
  readonly notify: SoftHitNotifier;
171
186
  readonly amount?: number;
187
+ readonly periodEndIso?: string;
188
+ readonly i18nKey?: string;
172
189
  readonly markSoftWarnedOutsideTransaction?: boolean;
173
190
  }): Promise<EnforceCapResult>;
174
191
  /**