@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
@@ -88,11 +88,15 @@ export const listQuery = defineQueryHandler({
88
88
  aggregateType: scopeFilter.aggregateType ?? p.aggregateType,
89
89
  }, direction);
90
90
  const rows = await selectMany(ctx.db, eventsTable, where, {
91
- // `id` as tie-breaker keeps the order total, so pages never overlap.
92
- orderBy: [
93
- { col: p.sort === "type" ? "type" : "createdAt", direction },
94
- { col: "id", direction },
95
- ],
91
+ // The id cursor only resumes an id-ordered listing. created_at comes from the
92
+ // transaction start and can run against insert order, so ordering by it would let
93
+ // a page boundary skip rows; only the type sort leaves id order (and is one page).
94
+ orderBy: p.sort === "type"
95
+ ? [
96
+ { col: "type", direction },
97
+ { col: "id", direction },
98
+ ]
99
+ : [{ col: "id", direction }],
96
100
  limit: p.limit,
97
101
  });
98
102
  const serialised = rows.map((r) => ({
@@ -1,7 +1,8 @@
1
- import { makeDispatchSystemWrite, type SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
1
+ import { makeDispatchSystemWrite, type RoleDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
- import { type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { type Registry, type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
5
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
5
6
  import type { Redis } from "ioredis";
6
7
  import type { GLOBAL_ROLE_OPTIONS } from "../user/constants.js";
7
8
  type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
@@ -18,9 +19,7 @@ export type BootstrapSeedDeps = {
18
19
  /** Writes as the system user with the given tenant roles in the seeded
19
20
  * tenant; use it when the app gates tenant data on its own roles rather
20
21
  * than SystemAdmin. */
21
- readonly dispatchWriteAs: (args: Pick<SystemDispatchArgs, "handlerQn" | "payload"> & {
22
- readonly roles: readonly string[];
23
- }) => Promise<WriteResult>;
22
+ readonly dispatchWriteAs: (args: Omit<RoleDispatchArgs, "tenantId">) => Promise<WriteResult>;
24
23
  };
25
24
  export type BootstrapConfigValue = string | number | boolean;
26
25
  export type BootstrapTenant = {
@@ -54,6 +53,10 @@ export type BootstrapDeps = {
54
53
  readonly db: DbConnection;
55
54
  readonly redis: Redis;
56
55
  readonly dispatcher: Dispatcher;
56
+ /** With these, a rerun can compare `encrypted`/`piiEncrypted` config keys by
57
+ * their decrypted value; without them such keys are rewritten on every run. */
58
+ readonly registry?: Registry;
59
+ readonly configCipher?: EnvelopeCipher;
57
60
  };
58
61
  export type BootstrapTenantOutcome = "created" | "exists";
59
62
  export type BootstrapInviteOutcome = "invited" | "resent" | "pending" | "active" | "cancelled" | "role-mismatch";
@@ -4,14 +4,14 @@
4
4
  // exactly as for any other writer. Nobody gets a role before accepting: the
5
5
  // invitation row holds membership role + global roles until the invitee
6
6
  // accepts through the regular invite-accept routes.
7
- import { makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
7
+ import { makeDispatchSystemWrite, makeDispatchWriteAs, } from "@cosmicdrift/kumiko-framework/api";
8
8
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
9
9
  import { ConfigScopes, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
10
10
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
11
11
  // kumiko-lint-ignore cross-feature-import bootstrap sets per-tenant config through the config feature's write handler
12
12
  import { ConfigHandlers } from "../config/constants.js";
13
13
  // kumiko-lint-ignore cross-feature-import bootstrap compares against the stored config row to stay idempotent
14
- import { findConfigRow } from "../config/write-helpers.js";
14
+ import { configRowHoldsValue, findConfigRow } from "../config/write-helpers.js";
15
15
  // kumiko-lint-ignore cross-feature-import bootstrap provisions tenant + invitation rows owned by the tenant feature
16
16
  import { TenantHandlers } from "../tenant/constants.js";
17
17
  // kumiko-lint-ignore cross-feature-import bootstrap provisions tenant + invitation rows owned by the tenant feature
@@ -78,8 +78,10 @@ async function applyTenantConfig(deps, tenant) {
78
78
  const applied = [];
79
79
  for (const [key, value] of Object.entries(tenant.config ?? {})) {
80
80
  const existing = await findConfigRow(deps.db, key, tenant.id, null);
81
- if (existing?.value === JSON.stringify(value))
81
+ const serialized = JSON.stringify(value);
82
+ if (await configRowHoldsValue(existing, deps.registry?.getConfigKey(key), deps.configCipher, key, serialized)) {
82
83
  continue;
84
+ }
83
85
  const result = await deps.dispatcher.write(ConfigHandlers.set, { key, value, scope: ConfigScopes.tenant },
84
86
  // SystemAdmin on top of the system role: keys with the default tenant
85
87
  // write access (admin roles) reject a bare system user, system-only keys
@@ -125,6 +127,11 @@ async function decideInvite(deps, invite) {
125
127
  }
126
128
  if (invitation.status === INVITATION_STATUS.accepted)
127
129
  return "active";
130
+ // The plan dropped a global role (e.g. an admin moved to tenants[].invites):
131
+ // the stored invitation would still grant it on accept, so it is re-issued.
132
+ if (storedGlobalRoles.some((role) => !invite.globalRoles.some((planned) => planned === role))) {
133
+ return "resent";
134
+ }
128
135
  if (await hasLiveInviteToken(deps.redis, invitation.id))
129
136
  return "pending";
130
137
  return "resent";
@@ -135,6 +142,7 @@ function isMailingOutcome(outcome) {
135
142
  export async function bootstrapTenants(deps, plan) {
136
143
  const plannedInvites = planInvites(plan);
137
144
  const dispatchSystemWrite = makeDispatchSystemWrite(deps.dispatcher);
145
+ const dispatchWriteAsRoles = makeDispatchWriteAs(deps.dispatcher);
138
146
  const tenants = [];
139
147
  for (const tenant of plan.tenants) {
140
148
  const outcome = await ensureTenant(deps, tenant);
@@ -144,7 +152,7 @@ export async function bootstrapTenants(deps, plan) {
144
152
  tenantId: tenant.id,
145
153
  db: deps.db,
146
154
  dispatchSystemWrite,
147
- dispatchWriteAs: ({ handlerQn, payload, roles }) => deps.dispatcher.write(handlerQn, payload, createSystemUser(tenant.id, roles)),
155
+ dispatchWriteAs: (args) => dispatchWriteAsRoles({ ...args, tenantId: tenant.id }),
148
156
  });
149
157
  }
150
158
  const configApplied = await applyTenantConfig(deps, tenant);
@@ -0,0 +1,4 @@
1
+ import type { PayloadRateLimitOption } from "@cosmicdrift/kumiko-types/handlers";
2
+ export declare const AUTH_EMAIL_RATE_LIMIT_WINDOW_SECONDS: number;
3
+ export declare const AUTH_EMAIL_RATE_LIMIT_MAX_REQUESTS = 3;
4
+ export declare const AUTH_EMAIL_PAYLOAD_RATE_LIMITS: readonly PayloadRateLimitOption[];
@@ -0,0 +1,13 @@
1
+ // The bucket counts across all IPs, so a long window would let an attacker who
2
+ // knows an address lock its owner out of recovery/signup for the whole window.
3
+ // A short one still throttles mail bombing but clears within minutes.
4
+ export const AUTH_EMAIL_RATE_LIMIT_WINDOW_SECONDS = 15 * 60;
5
+ export const AUTH_EMAIL_RATE_LIMIT_MAX_REQUESTS = 3;
6
+ export const AUTH_EMAIL_PAYLOAD_RATE_LIMITS = [
7
+ {
8
+ per: { payloadField: "email" },
9
+ limit: AUTH_EMAIL_RATE_LIMIT_MAX_REQUESTS,
10
+ windowSeconds: AUTH_EMAIL_RATE_LIMIT_WINDOW_SECONDS,
11
+ normalize: "email",
12
+ },
13
+ ];
@@ -4,4 +4,4 @@ export declare const changePasswordWrite: import("@cosmicdrift/kumiko-types/defi
4
4
  newPassword: z.ZodString;
5
5
  }, z.core.$strip>, {
6
6
  kind: string;
7
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
7
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,4 +1,4 @@
1
- import { access, createSystemUser, defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { createSystemUser, defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
3
  import { hashPassword, verifyPassword } from "../../shared/index.js";
4
4
  import { UserHandlers, UserQueries } from "../../user/index.js";
@@ -14,7 +14,11 @@ export const changePasswordWrite = defineWriteHandler({
14
14
  oldPassword: z.string().min(1),
15
15
  newPassword: passwordSchema,
16
16
  }),
17
- access: { roles: access.authenticated },
17
+ access: {
18
+ openToAll: {
19
+ reason: "Self-service: acts only on event.user.id and re-checks the caller's current password; invited members hold only an app role, so a fixed role list would lock them out.",
20
+ },
21
+ },
18
22
  escapeHatch: {
19
23
  grants: ["systemIdentity"],
20
24
  reason: "Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, ...) " +
@@ -6,4 +6,4 @@ export declare function createConfirmAccountUnlockHandler(opts: ConfirmAccountUn
6
6
  token: z.ZodString;
7
7
  }, z.core.$strip>, {
8
8
  kind: "account-unlocked";
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -18,4 +18,4 @@ export declare function createInviteAcceptWithLoginHandler(opts?: InviteAcceptWi
18
18
  token: z.ZodString;
19
19
  email: z.ZodEmail;
20
20
  password: z.ZodString;
21
- }, z.core.$strip>, InviteAcceptWithLoginData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
21
+ }, z.core.$strip>, InviteAcceptWithLoginData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -8,4 +8,4 @@ export type InviteAcceptData = {
8
8
  };
9
9
  export declare function createInviteAcceptHandler(): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"invite-accept", z.ZodObject<{
10
10
  token: z.ZodString;
11
- }, z.core.$strip>, InviteAcceptData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
11
+ }, z.core.$strip>, InviteAcceptData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -33,12 +33,12 @@ export type InviteCreateOptions = {
33
33
  export declare function createInviteCreateHandler(opts: InviteCreateOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"invite-create", z.ZodObject<{
34
34
  email: z.ZodEmail;
35
35
  role: z.ZodString;
36
- }, z.core.$strip>, InviteCreateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
36
+ }, z.core.$strip>, InviteCreateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
37
37
  export declare function createSystemInviteCreateHandler(opts: InviteCreateOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"system-invite-create", z.ZodObject<{
38
38
  email: z.ZodEmail;
39
39
  role: z.ZodString;
40
40
  globalRoles: z.ZodDefault<z.ZodArray<z.ZodEnum<{
41
41
  SystemAdmin: "SystemAdmin";
42
42
  }>>>;
43
- }, z.core.$strip>, InviteCreateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
43
+ }, z.core.$strip>, InviteCreateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
44
44
  export {};
@@ -13,4 +13,4 @@ export type InviteSignupCompleteOptions = Pick<LoginHandlerOptions, "mfaStatusCh
13
13
  export declare function createInviteSignupCompleteHandler(opts?: InviteSignupCompleteOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"invite-signup-complete", z.ZodObject<{
14
14
  token: z.ZodString;
15
15
  password: z.ZodString;
16
- }, z.core.$strip>, InviteSignupCompleteData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
16
+ }, z.core.$strip>, InviteSignupCompleteData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -74,5 +74,5 @@ export declare function gateBuildSession(ctx: HandlerContext, userId: string, te
74
74
  export declare function createLoginHandler(opts?: LoginHandlerOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"login", z.ZodObject<{
75
75
  email: z.ZodEmail;
76
76
  password: z.ZodString;
77
- }, z.core.$strip>, LoginResult, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
77
+ }, z.core.$strip>, LoginResult, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
78
78
  export {};
@@ -1,2 +1,2 @@
1
1
  import * as z from "zod";
2
- export declare const logoutWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"logout", z.ZodObject<{}, z.core.$strip>, unknown, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
2
+ export declare const logoutWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"logout", z.ZodObject<{}, z.core.$strip>, unknown, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,4 +1,4 @@
1
- import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
3
  // Logout — JWT is stateless, so server-side we only return OK. A future
4
4
  // revocation list / session table can land here without changing the
@@ -6,7 +6,11 @@ import * as z from "zod";
6
6
  export const logoutWrite = defineWriteHandler({
7
7
  name: "logout",
8
8
  schema: z.object({}),
9
- access: { roles: access.authenticated },
9
+ access: {
10
+ openToAll: {
11
+ reason: "Self-service: touches no data, any signed-in member must be able to sign out regardless of which role they hold.",
12
+ },
13
+ },
10
14
  description: "Acknowledges a sign-out for the authenticated caller so the client can discard its token; the JWT is stateless, so no server-side session state changes here.",
11
15
  perform: stepsPipeline(({ r }) => [
12
16
  r.step.return({ isSuccess: true, data: { kind: "logged-out" } }),
@@ -3,4 +3,4 @@ export type RequestAccountUnlockOptions = TokenRequestOptions;
3
3
  export type RequestUnlockData = TokenRequestData<"unlock-requested">;
4
4
  export declare function createRequestAccountUnlockHandler(opts: RequestAccountUnlockOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"request-account-unlock", import("zod").ZodObject<{
5
5
  email: import("zod").ZodEmail;
6
- }, import("zod/v4/core").$strip>, TokenRequestData<"unlock-requested">, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
6
+ }, import("zod/v4/core").$strip>, TokenRequestData<"unlock-requested">, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -3,4 +3,4 @@ export type RequestEmailVerificationOptions = TokenRequestOptions;
3
3
  export type RequestVerificationData = TokenRequestData<"verification-requested">;
4
4
  export declare function createRequestEmailVerificationHandler(opts: RequestEmailVerificationOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"request-email-verification", import("zod").ZodObject<{
5
5
  email: import("zod").ZodEmail;
6
- }, import("zod/v4/core").$strip>, TokenRequestData<"verification-requested">, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
6
+ }, import("zod/v4/core").$strip>, TokenRequestData<"verification-requested">, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -3,4 +3,4 @@ export type RequestPasswordResetOptions = TokenRequestOptions;
3
3
  export type RequestResetData = TokenRequestData<"reset-requested">;
4
4
  export declare function createRequestPasswordResetHandler(opts: RequestPasswordResetOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"request-password-reset", import("zod").ZodObject<{
5
5
  email: import("zod").ZodEmail;
6
- }, import("zod/v4/core").$strip>, TokenRequestData<"reset-requested">, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
6
+ }, import("zod/v4/core").$strip>, TokenRequestData<"reset-requested">, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -7,4 +7,4 @@ export declare function createResetPasswordHandler(opts: ResetPasswordOptions):
7
7
  newPassword: z.ZodString;
8
8
  }, z.core.$strip>, {
9
9
  kind: "password-reset";
10
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
10
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -27,5 +27,5 @@ type ClaimedHandover = {
27
27
  export declare function createSignupConfirmHandler(opts?: SignupConfirmOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"signup-confirm", z.ZodObject<{
28
28
  token: z.ZodString;
29
29
  password: z.ZodString;
30
- }, z.core.$strip>, SignupConfirmData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
30
+ }, z.core.$strip>, SignupConfirmData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
31
31
  export {};
@@ -29,4 +29,4 @@ export declare function createSignupRequestHandler(opts: SignupRequestOptions):
29
29
  entityType: z.ZodString;
30
30
  token: z.ZodString;
31
31
  }, z.core.$strip>>;
32
- }, z.core.$strip>, SignupRequestData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
32
+ }, z.core.$strip>, SignupRequestData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -24,6 +24,7 @@ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
24
24
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
25
25
  import * as z from "zod";
26
26
  import { findSignupHandoverProvider } from "../../shared/index.js";
27
+ import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.js";
27
28
  import { resolveHandlerMailLocale } from "../magic-link-mail.js";
28
29
  import { AUTH_SELF_REGISTRATION_FEATURE } from "../self-registration-toggle.js";
29
30
  import { issueSignupActivation } from "../signup-activation.js";
@@ -48,7 +49,7 @@ export function createSignupRequestHandler(opts) {
48
49
  schema: SignupRequestSchema,
49
50
  access: { roles: ["anonymous"] },
50
51
  rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
51
- additionalRateLimits: [{ per: { payloadField: "email" }, limit: 3, windowSeconds: 86400 }],
52
+ additionalRateLimits: AUTH_EMAIL_PAYLOAD_RATE_LIMITS,
52
53
  description: "Starts magic-link self-registration by mailing a fresh activation link to an address and invalidating any link still outstanding for it; the answer looks the same whether or not the address is already registered.",
53
54
  escapeHatch: { reason: HANDOVER_GRANT_VERIFY_REASON, grants: ["unsafeRaw"] },
54
55
  handler: async (event, ctx) => {
@@ -6,4 +6,4 @@ export type SwitchTenantMfaGateData = {
6
6
  export declare function createSwitchTenantMfaGateHandler(opts: Pick<LoginHandlerOptions, "mfaStatusChecker">): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"switch-tenant-mfa-gate", z.ZodObject<{
7
7
  userId: z.ZodString;
8
8
  tenantId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
9
- }, z.core.$strip>, SwitchTenantMfaGateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, z.core.$strip>, SwitchTenantMfaGateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -41,4 +41,4 @@ export type TokenRequestOptions = {
41
41
  };
42
42
  export declare function createTokenRequestHandler<TName extends string, TSuccessKind extends string>(spec: TokenRequestSpec<TName, TSuccessKind>, opts: TokenRequestOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<TName, z.ZodObject<{
43
43
  email: z.ZodEmail;
44
- }, z.core.$strip>, TokenRequestData<TSuccessKind>, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
44
+ }, z.core.$strip>, TokenRequestData<TSuccessKind>, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -17,6 +17,7 @@ import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
17
17
  import * as z from "zod";
18
18
  import { UserQueries } from "../../user/index.js";
19
19
  import { parseAuthUserRow } from "../auth-user-row.js";
20
+ import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.js";
20
21
  import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.js";
21
22
  const RequestTokenSchema = z.object({
22
23
  email: z.email(),
@@ -33,7 +34,7 @@ export function createTokenRequestHandler(spec, opts) {
33
34
  schema: RequestTokenSchema,
34
35
  access: { roles: ["anonymous"] },
35
36
  rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
36
- additionalRateLimits: [{ per: { payloadField: "email" }, limit: 5, windowSeconds: 86400 }],
37
+ additionalRateLimits: AUTH_EMAIL_PAYLOAD_RATE_LIMITS,
37
38
  escapeHatch: {
38
39
  grants: ["systemIdentity"],
39
40
  reason: "Anonymous requester has no session — looks up the target user by email via " +
@@ -9,4 +9,4 @@ export type VerifyEmailData = {
9
9
  };
10
10
  export declare function createVerifyEmailHandler(opts: VerifyEmailOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"verify-email", z.ZodObject<{
11
11
  token: z.ZodString;
12
- }, z.core.$strip>, VerifyEmailData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
12
+ }, z.core.$strip>, VerifyEmailData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -10,7 +10,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
10
10
  // (nur `{ children }`-Prop). Der Sample kann so einen eigenen Login-
11
11
  // Screen rein konfigurieren, ohne den Gate selbst ersetzen zu müssen.
12
12
  import { useLocale } from "@cosmicdrift/kumiko-renderer";
13
- import { useEffect, useState } from "react";
13
+ import { useEffect, useMemo, useState } from "react";
14
14
  import { assertNavigableUrl, buildLoginRedirectUrl, followNextOrLandingAfterLogin, } from "./auth-redirect.js";
15
15
  import { LoginScreen } from "./login-screen.js";
16
16
  import { SessionProvider, useSession } from "./session.js";
@@ -102,6 +102,17 @@ export function resolveLoginRedirect(loginUrl, locale, { origin, pathname, searc
102
102
  url: typeof loginUrl === "function" ? target : buildLoginRedirectUrl(target, returnPath, origin),
103
103
  };
104
104
  }
105
+ // A throwing loginUrl builder must not take the whole app tree down in render;
106
+ // null falls through to the built-in login screen.
107
+ function resolveLoginRedirectOrNull(loginUrl, locale, location) {
108
+ try {
109
+ return resolveLoginRedirect(loginUrl, locale, location);
110
+ }
111
+ catch (error) {
112
+ console.error("Auth gate: loginUrl could not be resolved, showing the built-in login", error);
113
+ return null;
114
+ }
115
+ }
105
116
  export function makeAuthGate(opts = {}) {
106
117
  const { loginUrl } = opts;
107
118
  if (typeof loginUrl === "string")
@@ -110,9 +121,10 @@ export function makeAuthGate(opts = {}) {
110
121
  function AuthGate({ children }) {
111
122
  const { status } = useSession();
112
123
  const locale = useLocale().locale();
113
- const loginRedirect = loginUrl !== undefined && status === "unauthenticated"
114
- ? resolveLoginRedirect(loginUrl, locale, window.location)
115
- : null;
124
+ const { origin, pathname, search, hash } = window.location;
125
+ const loginRedirect = useMemo(() => loginUrl !== undefined && status === "unauthenticated"
126
+ ? resolveLoginRedirectOrNull(loginUrl, locale, { origin, pathname, search, hash })
127
+ : null, [status, locale, origin, pathname, search, hash]);
116
128
  const redirectUrl = loginRedirect?.kind === "redirect" ? loginRedirect.url : null;
117
129
  // kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
118
130
  useEffect(() => {
@@ -7,4 +7,4 @@ export declare function createDisableHandler(opts: DisableOptions): import("@cos
7
7
  code: z.ZodString;
8
8
  }, z.core.$strip>, {
9
9
  disabled: boolean;
10
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
10
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -13,4 +13,4 @@ export declare function createEnableConfirmPreauthHandler(opts: EnableConfirmPre
13
13
  }, z.core.$strip>, {
14
14
  kind: string;
15
15
  session: SessionUser;
16
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
16
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -9,4 +9,4 @@ export declare function createEnableConfirmHandler(opts: EnableConfirmOptions):
9
9
  code: z.ZodString;
10
10
  }, z.core.$strip>, {
11
11
  enabled: boolean;
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 function createEnableStartPreauthHandler(opts: EnableStartPreauth
11
11
  setupToken: string;
12
12
  otpauthUri: string;
13
13
  recoveryCodes: string[];
14
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
14
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -10,4 +10,4 @@ export declare function createEnableStartHandler(opts: EnableStartOptions): impo
10
10
  otpauthUri: string;
11
11
  totpSecret: string;
12
12
  recoveryCodes: string[];
13
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
13
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare function createRegenerateRecoveryHandler(opts: RegenerateRecovery
6
6
  code: z.ZodString;
7
7
  }, z.core.$strip>, {
8
8
  recoveryCodes: string[];
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -11,4 +11,4 @@ export declare function createMfaVerifyHandler(opts: MfaVerifyOptions): import("
11
11
  }, z.core.$strip>, {
12
12
  kind: string;
13
13
  session: SessionUser;
14
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
14
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,4 +1,7 @@
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
1
2
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
+ import { Marked } from "marked";
4
+ import { isAllowedDocumentLinkHref, isDocumentPlaceholderHref, } from "../../template-resolver/index.js";
2
5
  import { resolveConsentLocale } from "./consent-text.js";
3
6
  export const CONFIRMATION_LABELS = {
4
7
  de: {
@@ -89,13 +92,57 @@ function formatDate(iso, locale, timeZone) {
89
92
  timeZoneName: "short",
90
93
  }).format(epochMilliseconds);
91
94
  }
92
- // The mail template only knows plain text variables, so the Markdown terms
93
- // become one row per paragraph; heading markers are dropped.
95
+ // One richText row of inline HTML per Markdown block. The sanitizer shows a rejected
96
+ // row as escaped text, so author HTML, images and refused links stay plain text.
97
+ const ROW_END = "\n\n";
98
+ function isMailLinkHref(href) {
99
+ return isAllowedDocumentLinkHref(href) && !isDocumentPlaceholderHref(href);
100
+ }
101
+ const inlineHtmlMarkdown = new Marked({ gfm: false, breaks: false });
102
+ inlineHtmlMarkdown.use({
103
+ renderer: {
104
+ heading({ tokens }) {
105
+ const headingHtml = this.parser.parseInline(tokens);
106
+ return `<strong>${headingHtml}</strong>${ROW_END}`;
107
+ },
108
+ paragraph({ tokens }) {
109
+ return `${this.parser.parseInline(tokens)}${ROW_END}`;
110
+ },
111
+ code: ({ text }) => `${escapeHtml(text)}${ROW_END}`,
112
+ blockquote({ tokens }) {
113
+ return this.parser.parse(tokens);
114
+ },
115
+ list({ items }) {
116
+ return items.map((item) => this.parser.parse(item.tokens)).join("");
117
+ },
118
+ hr: () => "",
119
+ space: () => "",
120
+ del({ tokens }) {
121
+ return this.parser.parseInline(tokens);
122
+ },
123
+ br: () => " ",
124
+ html: ({ text }) => escapeHtml(text),
125
+ image: ({ text }) => escapeHtml(text),
126
+ text(token) {
127
+ // Only block-level text (a list item's own text) carries inline child tokens.
128
+ return "tokens" in token && token.tokens
129
+ ? `${this.parser.parseInline(token.tokens)}${ROW_END}`
130
+ : escapeHtml(token.text);
131
+ },
132
+ link({ href, tokens }) {
133
+ const labelHtml = this.parser.parseInline(tokens);
134
+ if (isMailLinkHref(href))
135
+ return `<a href="${escapeHtmlAttr(href)}">${labelHtml}</a>`;
136
+ return labelHtml === escapeHtml(href) ? labelHtml : `${labelHtml} (${escapeHtml(href)})`;
137
+ },
138
+ },
139
+ });
94
140
  function termsParagraphsOf(markdown) {
95
- return markdown
141
+ return inlineHtmlMarkdown
142
+ .parse(markdown, { async: false })
96
143
  .split(/\n\s*\n/)
97
- .map((paragraph) => paragraph.replace(/^#{1,6}\s+/gm, "").trim())
98
- .filter((paragraph) => paragraph.length > 0)
144
+ .map((row) => row.replace(/\s+/g, " ").trim())
145
+ .filter((row) => row.length > 0)
99
146
  .map((text) => ({ text }));
100
147
  }
101
148
  export function contractConfirmationVariables(args) {
@@ -22,6 +22,9 @@ export type OrderItem = {
22
22
  readonly labelKey: string;
23
23
  readonly params?: Readonly<Record<string, string | number>>;
24
24
  };
25
+ /** Throws an InternalError (an app configuration fault, not a buyer error) for a
26
+ * label the consent event could not store. */
27
+ export declare function assertValidOrderItem(item: OrderItem | undefined, priceId: string): void;
25
28
  export type ConsentPriceDetails = {
26
29
  readonly tier: string | null;
27
30
  readonly priceId: string;
@@ -2,12 +2,12 @@
2
2
  // validate + prepare before the provider call, record only after it succeeded.
3
3
  import { createHash } from "node:crypto";
4
4
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
5
- import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
5
+ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
6
6
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
7
7
  import * as z from "zod";
8
8
  import { requireTemplateResolver, TEXT_BLOCK_KIND, TemplateNotFoundError, } from "../../template-resolver/index.js";
9
9
  import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
10
- import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, PAYMENT_AGGREGATE_TYPE, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
10
+ import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, checkoutConsentRecordedPayloadSchema, PAYMENT_AGGREGATE_TYPE, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
11
11
  import { consentTextVersion, resolveConsentLocale, submitMessageFor } from "./consent-text.js";
12
12
  // Optional at the schema level so a missing/unchecked consent reaches
13
13
  // prepareConsent and fails as 422 consent_required, not as a zod 400.
@@ -19,6 +19,25 @@ export const consentPayloadSchema = z
19
19
  locale: z.string().min(2).max(35),
20
20
  })
21
21
  .strict();
22
+ // Bounds come from the event schema the label ends up in, so an app label that
23
+ // passes here cannot fail the append after the provider session exists.
24
+ const orderItemSchema = z.object({
25
+ labelKey: checkoutConsentRecordedPayloadSchema.shape.itemLabelKey.unwrap(),
26
+ params: checkoutConsentRecordedPayloadSchema.shape.itemLabelParams,
27
+ });
28
+ /** Throws an InternalError (an app configuration fault, not a buyer error) for a
29
+ * label the consent event could not store. */
30
+ export function assertValidOrderItem(item, priceId) {
31
+ // skip: subscription checkouts have no one-off order item
32
+ if (item === undefined)
33
+ return;
34
+ const parsed = orderItemSchema.safeParse(item);
35
+ if (!parsed.success) {
36
+ throw new InternalError({
37
+ message: `billing-foundation: consumerProtection.oneOffItemLabel returned an invalid label for priceId "${priceId}": ${parsed.error.message}`,
38
+ });
39
+ }
40
+ }
22
41
  export async function prepareConsent(ctx, options, consent, mode) {
23
42
  if (!consent?.earlyPerformanceRequested || !consent.withdrawalLossAcknowledged) {
24
43
  throw new UnprocessableError("consent_required", {
@@ -21,7 +21,7 @@ export const OPERATOR_NOTICE_VARIABLES = {
21
21
  export const CONTRACT_CONFIRMATION_VARIABLES = {
22
22
  contractLines: textRows(),
23
23
  consentGivenAt: templateVariable.text({ required: true }),
24
- termsParagraphs: textRows(),
24
+ termsParagraphs: templateVariable.list({ text: templateVariable.richText({ required: true }) }, { required: true }),
25
25
  operatorEmail: templateVariable.text({ required: true }),
26
26
  };
27
27
  function perLocale(build) {
@@ -16,7 +16,17 @@ export function registerContractTermination(r, options, consumerProtection) {
16
16
  // request: matched and unmatched requests then do the same work and the
17
17
  // response time does not tell whether the email belongs to a customer.
18
18
  // The tenant comes from the stored trigger event (the job's system user).
19
- r.job("cancel-on-public-termination-declared", { trigger: { on: CONTRACT_TERMINATION_DECLARED_EVENT_QN }, runIn: "worker" }, async (payload, ctx) => {
19
+ // Every declaration appends to the same subscription stream, so parallel runs
20
+ // and a declaration arriving mid-run hit version conflicts. Sequential runs
21
+ // rule out the first; a retry re-reads the stream and the requestId check in
22
+ // record-contract-termination keeps it from recording twice.
23
+ r.job("cancel-on-public-termination-declared", {
24
+ trigger: { on: CONTRACT_TERMINATION_DECLARED_EVENT_QN },
25
+ runIn: "worker",
26
+ concurrency: "sequential",
27
+ retries: 3,
28
+ backoff: "exponential",
29
+ }, async (payload, ctx) => {
20
30
  const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
21
31
  // skip: trigger event carries no usable declaration
22
32
  if (!declared.success)