@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.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 (307) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/auth-email-password/email-templates.d.ts +4 -26
  8. package/dist/auth-email-password/email-templates.js +9 -101
  9. package/dist/auth-email-password/feature.js +24 -6
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  12. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  13. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  14. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
  16. package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
  17. package/dist/auth-email-password/index.d.ts +2 -2
  18. package/dist/auth-email-password/index.js +1 -1
  19. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  20. package/dist/auth-email-password/magic-link-mail.js +19 -14
  21. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  22. package/dist/auth-email-password/mail-notifications.js +116 -0
  23. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  24. package/dist/auth-email-password/signup-activation.js +2 -3
  25. package/dist/auth-foundation/types.js +2 -8
  26. package/dist/auth-mfa/feature.js +4 -4
  27. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  28. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  29. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  30. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  31. package/dist/billing-foundation/constants.d.ts +3 -3
  32. package/dist/billing-foundation/constants.js +5 -4
  33. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  34. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
  35. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  36. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  37. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  38. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  39. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
  40. package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
  41. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  42. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  43. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
  44. package/dist/billing-foundation/handlers/process-event.write.js +2 -2
  45. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  46. package/dist/billing-foundation/subscription-tier-sync.js +3 -4
  47. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  48. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  49. package/dist/channel-email/email-channel.d.ts +2 -2
  50. package/dist/channel-email/email-channel.js +89 -26
  51. package/dist/channel-email/feature.js +3 -3
  52. package/dist/channel-email/index.d.ts +1 -1
  53. package/dist/channel-email/index.js +1 -1
  54. package/dist/channel-email/pii-guard.js +10 -0
  55. package/dist/channel-email/smtp-transport.js +13 -0
  56. package/dist/channel-email/types.d.ts +8 -0
  57. package/dist/config/handlers/set.write.js +1 -2
  58. package/dist/custom-fields/lib/value-schema.js +10 -11
  59. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  60. package/dist/data-retention/index.d.ts +1 -0
  61. package/dist/data-retention/index.js +1 -0
  62. package/dist/data-retention/run-retention-cleanup.js +6 -6
  63. package/dist/delivery/attachment-ref.d.ts +39 -0
  64. package/dist/delivery/attachment-ref.js +64 -0
  65. package/dist/delivery/channel-context.d.ts +8 -4
  66. package/dist/delivery/channel-context.js +76 -2
  67. package/dist/delivery/constants.d.ts +1 -0
  68. package/dist/delivery/constants.js +1 -0
  69. package/dist/delivery/delivery-service.d.ts +4 -1
  70. package/dist/delivery/delivery-service.js +38 -32
  71. package/dist/delivery/feature.js +1 -1
  72. package/dist/delivery/index.d.ts +5 -2
  73. package/dist/delivery/index.js +4 -1
  74. package/dist/delivery/jobs.js +25 -5
  75. package/dist/delivery/notify-factory.js +4 -1
  76. package/dist/delivery/testing.d.ts +4 -1
  77. package/dist/delivery/types.d.ts +20 -14
  78. package/dist/delivery/unsubscribe.js +4 -1
  79. package/dist/derivatives-sharp/render.js +1 -18
  80. package/dist/document-ingest-foundation/pages.js +4 -4
  81. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  82. package/dist/file-foundation/index.d.ts +1 -0
  83. package/dist/file-foundation/index.js +1 -0
  84. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  85. package/dist/file-foundation/read-file-ref-content.js +49 -0
  86. package/dist/folders/web/folder-manager.js +3 -2
  87. package/dist/form-draft/db/queries/cleanup.js +4 -4
  88. package/dist/form-draft/db/queries/draft-count.js +2 -2
  89. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  90. package/dist/form-draft/release-file-refs.js +3 -2
  91. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  92. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  93. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  94. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  95. package/dist/inbound-mail-foundation/types.js +4 -4
  96. package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
  97. package/dist/inbound-provider-imap/imap-client.js +4 -3
  98. package/dist/ledger/recurring.js +1 -3
  99. package/dist/managed-pages/branding.d.ts +2 -0
  100. package/dist/managed-pages/branding.js +24 -2
  101. package/dist/managed-pages/feature.js +1 -0
  102. package/dist/managed-pages/i18n.js +1 -0
  103. package/dist/managed-pages/screens/branding-screen.js +8 -1
  104. package/dist/page-render/branding.d.ts +1 -0
  105. package/dist/page-render/branding.js +1 -0
  106. package/dist/page-render/markdown.js +1 -1
  107. package/dist/personal-access-tokens/feature.js +1 -1
  108. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  109. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  110. package/dist/renderer-foundation/api.d.ts +13 -13
  111. package/dist/renderer-foundation/api.js +1 -1
  112. package/dist/renderer-foundation/constants.js +2 -4
  113. package/dist/renderer-foundation/feature.js +7 -7
  114. package/dist/renderer-foundation/index.d.ts +2 -1
  115. package/dist/renderer-foundation/index.js +1 -0
  116. package/dist/renderer-foundation/types.d.ts +92 -32
  117. package/dist/renderer-pdf/feature.js +7 -1
  118. package/dist/sessions/db/queries/cleanup.js +2 -2
  119. package/dist/sessions/feature.js +1 -1
  120. package/dist/sessions/handlers/list.query.js +2 -1
  121. package/dist/shared/attachment-file-name.d.ts +3 -0
  122. package/dist/shared/attachment-file-name.js +35 -0
  123. package/dist/shared/index.d.ts +3 -0
  124. package/dist/shared/index.js +3 -0
  125. package/dist/shared/is-tenant-db.js +1 -1
  126. package/dist/shared/mail-template-body.d.ts +4 -0
  127. package/dist/shared/mail-template-body.js +17 -0
  128. package/dist/shared/sharp-input-limits.d.ts +6 -0
  129. package/dist/shared/sharp-input-limits.js +22 -0
  130. package/dist/shared/signup-handover.js +2 -2
  131. package/dist/tags/handlers/assign-tag.write.js +3 -1
  132. package/dist/tags/web/tag-chip.js +2 -0
  133. package/dist/template-resolver/api.d.ts +30 -2
  134. package/dist/template-resolver/api.js +40 -5
  135. package/dist/template-resolver/compile-document-body.d.ts +8 -0
  136. package/dist/template-resolver/compile-document-body.js +163 -0
  137. package/dist/template-resolver/constants.d.ts +13 -4
  138. package/dist/template-resolver/constants.js +30 -10
  139. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  140. package/dist/template-resolver/document-sanitizer.js +201 -0
  141. package/dist/template-resolver/document-schema.d.ts +21 -0
  142. package/dist/template-resolver/document-schema.js +296 -0
  143. package/dist/template-resolver/feature.d.ts +18 -0
  144. package/dist/template-resolver/feature.js +50 -6
  145. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  146. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  147. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  148. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  149. package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
  150. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  151. package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
  152. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  153. package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
  154. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  155. package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
  156. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  157. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  158. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  159. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  160. package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
  161. package/dist/template-resolver/handlers/layout-source.query.js +64 -0
  162. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  163. package/dist/template-resolver/handlers/list.query.js +5 -7
  164. package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
  165. package/dist/template-resolver/handlers/overview.query.js +165 -0
  166. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  167. package/dist/template-resolver/handlers/preview.query.js +24 -0
  168. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  169. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  170. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  171. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  172. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  173. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  174. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  175. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  176. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  177. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  178. package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
  179. package/dist/template-resolver/handlers/send-test.write.js +81 -0
  180. package/dist/template-resolver/handlers/set.write.d.ts +4 -1
  181. package/dist/template-resolver/handlers/set.write.js +25 -3
  182. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  183. package/dist/template-resolver/handlers/shared.js +6 -2
  184. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  185. package/dist/template-resolver/handlers/template-gates.js +131 -0
  186. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  187. package/dist/template-resolver/handlers/template-write.js +109 -0
  188. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
  189. package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
  190. package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
  191. package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
  192. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  193. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  194. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  195. package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
  196. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  197. package/dist/template-resolver/handlers/usage.query.js +15 -0
  198. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  199. package/dist/template-resolver/handlers/validate.query.js +80 -0
  200. package/dist/template-resolver/index.d.ts +18 -1
  201. package/dist/template-resolver/index.js +16 -1
  202. package/dist/template-resolver/layout-regions.d.ts +23 -0
  203. package/dist/template-resolver/layout-regions.js +125 -0
  204. package/dist/template-resolver/linked-resources.d.ts +17 -0
  205. package/dist/template-resolver/linked-resources.js +75 -0
  206. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  207. package/dist/template-resolver/preview-display-html.js +27 -0
  208. package/dist/template-resolver/print-document.d.ts +1 -0
  209. package/dist/template-resolver/print-document.js +55 -0
  210. package/dist/template-resolver/qualified-names.d.ts +30 -0
  211. package/dist/template-resolver/qualified-names.js +30 -0
  212. package/dist/template-resolver/record-mapping.d.ts +2 -0
  213. package/dist/template-resolver/record-mapping.js +21 -0
  214. package/dist/template-resolver/resource-image.d.ts +15 -0
  215. package/dist/template-resolver/resource-image.js +34 -0
  216. package/dist/template-resolver/seeding.d.ts +14 -1
  217. package/dist/template-resolver/seeding.js +105 -15
  218. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  219. package/dist/template-resolver/store-template-resource.js +264 -0
  220. package/dist/template-resolver/table.d.ts +47 -8
  221. package/dist/template-resolver/table.js +11 -0
  222. package/dist/template-resolver/template-bundle.d.ts +36 -0
  223. package/dist/template-resolver/template-bundle.js +60 -0
  224. package/dist/template-resolver/template-diff.d.ts +7 -0
  225. package/dist/template-resolver/template-diff.js +68 -0
  226. package/dist/template-resolver/template-meta.d.ts +60 -0
  227. package/dist/template-resolver/template-meta.js +101 -0
  228. package/dist/template-resolver/template-preview.d.ts +86 -0
  229. package/dist/template-resolver/template-preview.js +374 -0
  230. package/dist/template-resolver/template-resources.d.ts +14 -0
  231. package/dist/template-resolver/template-resources.js +42 -0
  232. package/dist/template-resolver/template-usage.d.ts +12 -0
  233. package/dist/template-resolver/template-usage.js +30 -0
  234. package/dist/template-resolver/template-validation.d.ts +22 -0
  235. package/dist/template-resolver/template-validation.js +172 -0
  236. package/dist/template-resolver/testing.js +5 -5
  237. package/dist/template-resolver/user-content-table.d.ts +14 -8
  238. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  239. package/dist/template-resolver/web/client-plugin.js +9 -6
  240. package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
  241. package/dist/template-resolver/web/document-editor-extensions.js +107 -0
  242. package/dist/template-resolver/web/document-editor.d.ts +3 -0
  243. package/dist/template-resolver/web/document-editor.js +11 -0
  244. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  245. package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
  246. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
  247. package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
  248. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  249. package/dist/template-resolver/web/editor-handle.js +24 -0
  250. package/dist/template-resolver/web/i18n.js +76 -0
  251. package/dist/template-resolver/web/index.d.ts +4 -0
  252. package/dist/template-resolver/web/index.js +4 -0
  253. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  254. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  255. package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
  256. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  257. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  258. package/dist/template-resolver/web/placeholder-text.js +34 -0
  259. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  260. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  261. package/dist/tenant/handlers/team-list.query.js +3 -2
  262. package/dist/tenant/seeding.js +31 -6
  263. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  264. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  265. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  266. package/dist/user/handlers/detail.query.js +2 -1
  267. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  268. package/dist/user-data-rights/email-templates.d.ts +1 -29
  269. package/dist/user-data-rights/email-templates.js +5 -106
  270. package/dist/user-data-rights/feature.js +53 -35
  271. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  272. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  273. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  274. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  275. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  276. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  277. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  278. package/dist/user-data-rights/mail-notifications.js +118 -0
  279. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  280. package/dist/user-profile/web/profile-screen.js +2 -1
  281. package/dist/waitlist/feature.js +10 -0
  282. package/dist/waitlist/handlers/submit.write.js +3 -4
  283. package/dist/waitlist/mail.d.ts +19 -6
  284. package/dist/waitlist/mail.js +50 -14
  285. package/package.json +28 -17
  286. package/src/admin-shell/changes.json +6 -0
  287. package/src/auth-email-password/changes.json +14 -0
  288. package/src/billing-foundation/changes.json +7 -0
  289. package/src/channel-email/changes.json +19 -0
  290. package/src/data-retention/changes.json +6 -0
  291. package/src/delivery/changes.json +48 -0
  292. package/src/managed-pages/changes.json +6 -0
  293. package/src/renderer-foundation/changes.json +39 -0
  294. package/src/template-resolver/changes.json +118 -0
  295. package/src/user-data-rights/changes.json +7 -0
  296. package/src/waitlist/changes.json +7 -0
  297. package/dist/renderer-simple/feature.d.ts +0 -10
  298. package/dist/renderer-simple/feature.js +0 -42
  299. package/dist/renderer-simple/index.d.ts +0 -3
  300. package/dist/renderer-simple/index.js +0 -2
  301. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  302. package/dist/renderer-simple/mail-blocks.js +0 -107
  303. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  304. package/dist/renderer-simple/resolve-variables.js +0 -47
  305. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  306. package/dist/renderer-simple/simple-renderer.js +0 -200
  307. package/src/renderer-simple/changes.json +0 -27
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @runtime client
3
+ //
4
+ // Single-line editor for plain-text fields that carry {{placeholders}}
5
+ // (subject, preheader). The stored value stays plain text; placeholders show
6
+ // as chips while editing. Lives in the same lazy chunk as the document editor.
7
+ import { cn } from "@cosmicdrift/kumiko-renderer-web";
8
+ import { Extension, Node } from "@tiptap/core";
9
+ import { UndoRedo } from "@tiptap/extensions";
10
+ import { EditorContent, useEditor } from "@tiptap/react";
11
+ import StarterKit from "@tiptap/starter-kit";
12
+ import { useMemo, useState } from "react";
13
+ import { DocumentVariable } from "../document-schema.js";
14
+ import { createVariableView } from "./document-editor-extensions.js";
15
+ import { createEditorHandle } from "./editor-handle.js";
16
+ import { contentToLine, lineToContent, singleLine } from "./placeholder-text.js";
17
+ import { useStableByKey } from "./use-stable-by-key.js";
18
+ const LineDocument = Node.create({ name: "doc", topNode: true, content: "paragraph" });
19
+ // One line only: Enter must not split the paragraph.
20
+ const BlockLineBreaks = Extension.create({
21
+ name: "blockLineBreaks",
22
+ addKeyboardShortcuts() {
23
+ return { Enter: () => true, "Shift-Enter": () => true, "Mod-Enter": () => true };
24
+ },
25
+ });
26
+ const NO_LABELS = {};
27
+ const NO_NAMES = [];
28
+ function LineEditorBody({ id, initialValue, onChange, variableLabels, systemVariables, readOnly, onEditorReady, ariaLabel, }) {
29
+ const extensions = useMemo(() => [
30
+ LineDocument,
31
+ StarterKit.configure({
32
+ document: false,
33
+ blockquote: false,
34
+ bold: false,
35
+ bulletList: false,
36
+ code: false,
37
+ codeBlock: false,
38
+ dropcursor: false,
39
+ gapcursor: false,
40
+ hardBreak: false,
41
+ heading: false,
42
+ horizontalRule: false,
43
+ italic: false,
44
+ link: false,
45
+ listItem: false,
46
+ listKeymap: false,
47
+ orderedList: false,
48
+ strike: false,
49
+ trailingNode: false,
50
+ underline: false,
51
+ undoRedo: false,
52
+ }),
53
+ DocumentVariable.extend({
54
+ addNodeView: () => createVariableView(variableLabels, systemVariables),
55
+ }),
56
+ UndoRedo,
57
+ BlockLineBreaks,
58
+ ], [variableLabels, systemVariables]);
59
+ const editor = useEditor({
60
+ extensions,
61
+ content: {
62
+ type: "doc",
63
+ content: [{ type: "paragraph", content: lineToContent(initialValue) }],
64
+ },
65
+ editable: !readOnly,
66
+ immediatelyRender: false,
67
+ editorProps: {
68
+ attributes: { id, role: "textbox", "aria-label": ariaLabel, "aria-multiline": "false" },
69
+ transformPastedText: singleLine,
70
+ // Rich pastes would bring several paragraphs; only their text is kept.
71
+ transformPastedHTML: (html) => singleLine(html.replace(/<[^>]*>/g, " ")),
72
+ },
73
+ onCreate: ({ editor: created }) => onEditorReady?.(createEditorHandle(created)),
74
+ onUpdate: ({ editor: e }) => onChange(contentToLine(e.getJSON().content?.[0]?.content)),
75
+ });
76
+ return (_jsx(EditorContent, { editor: editor, className: cn("flex min-h-9 items-center rounded-md border border-input bg-transparent px-3 text-base md:text-sm", "[&_.ProseMirror]:w-full [&_.ProseMirror]:whitespace-nowrap [&_.ProseMirror]:outline-none", "[&_p]:m-0", readOnly && "opacity-60") }));
77
+ }
78
+ export default function PlaceholderLineEditor({ id, value, onChange, variableLabels = NO_LABELS, systemVariables = NO_NAMES, readOnly = false, onEditorReady, "aria-label": ariaLabel, }) {
79
+ const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
80
+ const stableSystem = useStableByKey(systemVariables, systemVariables.join("\n"));
81
+ // The text is the source of truth. Typing reports it through onChange and
82
+ // keeps `emitted` equal to `value`; any other value (async load, reset)
83
+ // remounts the editor body on the new text.
84
+ const [emitted, setEmitted] = useState(value);
85
+ const [epoch, setEpoch] = useState(0);
86
+ if (value !== emitted) {
87
+ setEmitted(value);
88
+ setEpoch((current) => current + 1);
89
+ }
90
+ return (_jsx(LineEditorBody, { id: id, initialValue: value, onChange: (next) => {
91
+ setEmitted(next);
92
+ onChange(next);
93
+ }, variableLabels: stableLabels, systemVariables: stableSystem, readOnly: readOnly, onEditorReady: onEditorReady, ariaLabel: ariaLabel }, `${epoch}-${readOnly}-${JSON.stringify(stableLabels)}`));
94
+ }
@@ -0,0 +1,4 @@
1
+ import type { JSONContent } from "@tiptap/core";
2
+ export declare function lineToContent(text: string): JSONContent[];
3
+ export declare function contentToLine(nodes: readonly JSONContent[] | undefined): string;
4
+ export declare function singleLine(text: string): string;
@@ -0,0 +1,34 @@
1
+ import { isDocumentReferenceName } from "../document-schema.js";
2
+ const PLACEHOLDER_TOKEN = /\{\{([^{}]*)\}\}/g;
3
+ // Names the schema refuses stay plain text, so a typo never becomes a chip
4
+ // that the server would reject.
5
+ export function lineToContent(text) {
6
+ const content = [];
7
+ let cursor = 0;
8
+ const pushText = (value) => {
9
+ if (value !== "")
10
+ content.push({ type: "text", text: value });
11
+ };
12
+ for (const match of text.matchAll(PLACEHOLDER_TOKEN)) {
13
+ const name = match[1] ?? "";
14
+ if (!isDocumentReferenceName(name))
15
+ continue;
16
+ pushText(text.slice(cursor, match.index));
17
+ content.push({ type: "variable", attrs: { name } });
18
+ cursor = match.index + match[0].length;
19
+ }
20
+ pushText(text.slice(cursor));
21
+ return content;
22
+ }
23
+ export function contentToLine(nodes) {
24
+ return (nodes ?? [])
25
+ .map((node) => {
26
+ if (node.type === "variable")
27
+ return `{{${String(node.attrs?.["name"])}}}`;
28
+ return node.type === "text" ? (node.text ?? "") : "";
29
+ })
30
+ .join("");
31
+ }
32
+ export function singleLine(text) {
33
+ return text.replace(/\s*[\r\n]+\s*/g, " ");
34
+ }
@@ -0,0 +1 @@
1
+ export declare function useStableByKey<T>(value: T, key: string): T;
@@ -0,0 +1,13 @@
1
+ // @runtime client
2
+ import { useState } from "react";
3
+ // Callers often pass fresh object literals every render; this keeps the first
4
+ // value for as long as the content key is unchanged, so memoized extension
5
+ // lists (and the editor built from them) are not rebuilt on every render.
6
+ export function useStableByKey(value, key) {
7
+ const [stored, setStored] = useState({ key, value });
8
+ if (stored.key !== key) {
9
+ setStored({ key, value });
10
+ return value;
11
+ }
12
+ return stored.value;
13
+ }
@@ -3,6 +3,7 @@ import { access, definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/k
3
3
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
5
5
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
+ import { errorCodeOf, isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  import * as z from "zod";
7
8
  import { userSessionTable } from "../../sessions/index.js";
8
9
  import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.js";
@@ -15,7 +16,7 @@ const KMS_POOL_CONCURRENCY = 4;
15
16
  // the wire already names a TeamRow field directly.
16
17
  const SORT_FIELDS = ["email", "status", "createdAt", "lastSeenAt"];
17
18
  function isSortField(value) {
18
- return SORT_FIELDS.includes(value);
19
+ return isOneOf(SORT_FIELDS, value);
19
20
  }
20
21
  // Nulls (no session yet, invitation rows) sort last regardless of
21
22
  // direction — "unknown" reads as "least recent", not as a magic minimum
@@ -109,7 +110,7 @@ export const teamListQuery = definePagedQueryHandler({
109
110
  // ponytail: lean sample apps (admin-console) mount auth without
110
111
  // sessions — store_user_sessions never gets pushed. Integration tests
111
112
  // may seed the table without mounting the feature; prod mounts both.
112
- if (err.code !== "42P01")
113
+ if (errorCodeOf(err) !== "42P01")
113
114
  throw err;
114
115
  }
115
116
  }
@@ -31,7 +31,7 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
31
31
  import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
32
32
  import { createSystemUser, HookPhases, } from "@cosmicdrift/kumiko-framework/engine";
33
33
  import { getUnscopedAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
34
- import { createLifecycleHooks } from "@cosmicdrift/kumiko-framework/pipeline";
34
+ import { createDeferringNotify, createLifecycleHooks, } from "@cosmicdrift/kumiko-framework/pipeline";
35
35
  import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
36
36
  import { assertAssignableMembershipRoles } from "./membership-roles.js";
37
37
  import { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table.js";
@@ -62,9 +62,31 @@ const executor = createEventStoreExecutor(tenantMembershipsTable, tenantMembersh
62
62
  // dispatcher's sink and flush post-commit. Without a sink (fixture / plain
63
63
  // Connection) they still fire immediately — there is no outer TX to wait for.
64
64
  //
65
+ function buildSeedHookNotify(context, scopedUser, targetTenantId, notifyTiming) {
66
+ const notifyFactory = context._notifyFactory;
67
+ if (!notifyFactory)
68
+ return context.notify;
69
+ if (notifyTiming === "inline")
70
+ return notifyFactory(scopedUser, targetTenantId);
71
+ // Still inside the caller's write tx: delivery DB work must not wait for an app-pool slot.
72
+ const notifyInsideTransaction = (context._notifyInTransactionFactory ?? notifyFactory)(scopedUser, targetTenantId);
73
+ const scheduleAfterCommit = context.scheduleAfterCommit;
74
+ if (!scheduleAfterCommit)
75
+ return notifyInsideTransaction;
76
+ return createDeferringNotify({
77
+ registry: context.registry,
78
+ tenantId: targetTenantId,
79
+ notifyAfterCommit: notifyFactory(scopedUser, targetTenantId),
80
+ notifyNow: notifyInsideTransaction,
81
+ scheduleAfterCommit,
82
+ });
83
+ }
65
84
  // `runner` comes from the caller, which already holds a declared DbRunner —
66
85
  // a TenantDb exposes no runner back out to feature code.
67
- function scopeSeedHookContext(context, targetTenantId, runner) {
86
+ function scopeSeedHookContext(context, targetTenantId, runner,
87
+ // The afterCommit sink is snapshotted when it flushes, so a hook scheduled from inside an
88
+ // afterCommit-phase hook would be dropped: that phase must notify inline.
89
+ notifyTiming) {
68
90
  const db = createTenantDb(runner, targetTenantId, "system", context.tracer, context.meter, context.signal);
69
91
  // HandlerContext carries `user`; AppContext only `systemUser`. Prefer the
70
92
  // live handler user when present so notify/hasFeature keep a real SessionUser.
@@ -73,8 +95,8 @@ function scopeSeedHookContext(context, targetTenantId, runner) {
73
95
  const scopedUser = baseUser
74
96
  ? { ...baseUser, tenantId: targetTenantId }
75
97
  : undefined;
76
- const notify = context._notifyFactory && scopedUser
77
- ? context._notifyFactory(scopedUser, targetTenantId)
98
+ const notify = scopedUser
99
+ ? buildSeedHookNotify(context, scopedUser, targetTenantId, notifyTiming)
78
100
  : context.notify;
79
101
  const features = context.effectiveFeatures;
80
102
  const hasFeature = features
@@ -109,9 +131,12 @@ target) {
109
131
  const lifecycle = createLifecycleHooks(hooks.registry);
110
132
  const scopedContext = target === undefined
111
133
  ? hooks.context
112
- : scopeSeedHookContext(hooks.context, target.tenantId, target.db);
134
+ : scopeSeedHookContext(hooks.context, target.tenantId, target.db, "deferUntilCommit");
135
+ const afterCommitContext = target === undefined
136
+ ? hooks.context
137
+ : scopeSeedHookContext(hooks.context, target.tenantId, target.db, "inline");
113
138
  await lifecycle.runPostSave(pseudoType, entityData, scopedContext, HookPhases.inTransaction);
114
- const runAfterCommit = () => lifecycle.runPostSave(pseudoType, entityData, scopedContext, HookPhases.afterCommit);
139
+ const runAfterCommit = () => lifecycle.runPostSave(pseudoType, entityData, afterCommitContext, HookPhases.afterCommit);
115
140
  if (scopedContext.scheduleAfterCommit) {
116
141
  scopedContext.scheduleAfterCommit(runAfterCommit);
117
142
  }
@@ -2,5 +2,5 @@ import * as z from "zod";
2
2
  export declare const cancelDestructionWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"cancel-destruction", z.ZodObject<{}, z.core.$strip>, {
3
3
  tenantId: string;
4
4
  status: "active";
5
- gracePeriodEnd: string | null;
5
+ gracePeriodEnd: null;
6
6
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -52,12 +52,13 @@ export const cancelDestructionWrite = defineWriteHandler({
52
52
  type: DESTRUCTION_CANCELLED_EVENT_QN,
53
53
  payload: { cancelledBy: event.user.id },
54
54
  });
55
+ const gracePeriodEnd = null;
55
56
  return {
56
57
  isSuccess: true,
57
58
  data: {
58
59
  tenantId,
59
60
  status: "active",
60
- gracePeriodEnd: null,
61
+ gracePeriodEnd,
61
62
  },
62
63
  };
63
64
  },
@@ -26,8 +26,8 @@
26
26
  import { asRawClient, transaction, unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/bun-db";
27
27
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
28
28
  export async function backfillUserStreamTenants(db) {
29
- const candidates = (await unsafeReadRetrying(db, `SELECT DISTINCT "aggregate_id" FROM "kumiko_events"
30
- WHERE "aggregate_type" = 'user' AND "tenant_id" <> $1::uuid`, [SYSTEM_TENANT_ID]));
29
+ const candidates = await unsafeReadRetrying(db, `SELECT DISTINCT "aggregate_id" FROM "kumiko_events"
30
+ WHERE "aggregate_type" = 'user' AND "tenant_id" <> $1::uuid`, [SYSTEM_TENANT_ID]);
31
31
  let aggregatesMigrated = 0;
32
32
  let eventsMigrated = 0;
33
33
  const failed = [];
@@ -50,10 +50,10 @@ async function migrateAggregate(db, aggregateId) {
50
50
  const raw = asRawClient(tx);
51
51
  // Global id order = commit-adjacent replay order; merges a split stream
52
52
  // (legacy tenant + SYSTEM) into one consistent version sequence.
53
- const events = (await raw.unsafe(`SELECT "id" FROM "kumiko_events"
53
+ const events = await raw.unsafe(`SELECT "id" FROM "kumiko_events"
54
54
  WHERE "aggregate_type" = 'user' AND "aggregate_id" = $1::uuid
55
55
  ORDER BY "id" ASC
56
- FOR UPDATE`, [aggregateId]));
56
+ FOR UPDATE`, [aggregateId]);
57
57
  // Phase 1: negative interim versions — real versions are always >= 1, so
58
58
  // nothing can collide with events_aggregate_version_uq mid-migration.
59
59
  for (const [i, ev] of events.entries()) {
@@ -1,4 +1,5 @@
1
1
  import { access, defineEntityDetailHandler, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { userEntity } from "../schema/user.js";
3
4
  import { attachTenantLabels, dbForList } from "./list.query.js";
4
5
  // Only SystemAdmins can read arbitrary users. Tenant-level "Admin" does NOT
@@ -14,7 +15,7 @@ export const detailQuery = {
14
15
  ...baseDetail,
15
16
  handler: async (query, ctx) => {
16
17
  const result = await baseDetail.handler?.(query, ctx);
17
- if (result === null || typeof result !== "object")
18
+ if (!isPlainObject(result))
18
19
  return result;
19
20
  if (!ctx.registry.features.has("tenant"))
20
21
  return result;
@@ -1,5 +1,5 @@
1
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
1
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
- export declare function selectUsersDueForForgetCleanup(db: DbConnection, status: string, gracePeriodEndCutoff: Temporal.Instant | string): Promise<readonly {
3
+ export declare function selectUsersDueForForgetCleanup(db: DbRunner, status: string, gracePeriodEndCutoff: Temporal.Instant | string): Promise<readonly {
4
4
  id: string;
5
5
  }[]>;
@@ -1,32 +1,4 @@
1
1
  export type GdprMailLocale = string;
2
2
  export declare function normalizeGdprMailLocale(raw: string | null | undefined): GdprMailLocale | undefined;
3
3
  export declare const GDPR_MAIL_EN: Readonly<Record<string, string>>;
4
- export type RenderedEmail = {
5
- readonly subject: string;
6
- readonly html: string;
7
- readonly text: string;
8
- };
9
- export type RenderExportReadyEmailArgs = {
10
- readonly downloadUrl: string;
11
- readonly expiresAt: string;
12
- readonly locale?: GdprMailLocale;
13
- readonly appName?: string;
14
- };
15
- export type RenderExportFailedEmailArgs = {
16
- readonly locale?: GdprMailLocale;
17
- readonly appName?: string;
18
- };
19
- export type RenderDeletionRequestedEmailArgs = {
20
- readonly gracePeriodEnd: string;
21
- readonly locale?: GdprMailLocale;
22
- readonly appName?: string;
23
- };
24
- export type RenderDeletionExecutedEmailArgs = {
25
- readonly executedAt: string;
26
- readonly locale?: GdprMailLocale;
27
- readonly appName?: string;
28
- };
29
- export declare function renderExportReadyEmail(args: RenderExportReadyEmailArgs): RenderedEmail;
30
- export declare function renderExportFailedEmail(args: RenderExportFailedEmailArgs): RenderedEmail;
31
- export declare function renderDeletionRequestedEmail(args: RenderDeletionRequestedEmailArgs): RenderedEmail;
32
- export declare function renderDeletionExecutedEmail(args: RenderDeletionExecutedEmailArgs): RenderedEmail;
4
+ export declare function formatGdprMailTimestamp(iso: string): string;
@@ -1,9 +1,9 @@
1
- // Default HTML renderers for GDPR notification mails. English ships here;
2
- // other languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
1
+ // Translations for the GDPR notification mails (the templates in
2
+ // mail-notifications.ts are built from them). English ships here; other
3
+ // languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
3
4
  // localeEs() call registerMailTranslations). Apps that want their own
4
5
  // branding pass send*Email callbacks and these defaults do not run.
5
- import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
6
- import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
6
+ import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
7
7
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
8
8
  export function normalizeGdprMailLocale(raw) {
9
9
  if (!raw)
@@ -28,108 +28,7 @@ export const GDPR_MAIL_EN = {
28
28
  "gdpr.mail.fallbackUrl": "If the button doesn't work, copy this link into your browser:",
29
29
  };
30
30
  registerMailTranslations("en", GDPR_MAIL_EN);
31
- function t(locale, key, params) {
32
- return mailT(locale, key, params);
33
- }
34
- function appNameFor(args) {
35
- const locale = args.locale ?? "en";
36
- return args.appName ?? t(locale, "gdpr.mail.appNameDefault");
37
- }
38
- export function renderExportReadyEmail(args) {
39
- const locale = args.locale ?? "en";
40
- const app = appNameFor(args);
41
- const subject = t(locale, "gdpr.mail.exportReady.subject", { app });
42
- const body = `
43
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
44
- <p style="margin: 0 0 24px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportReady.intro", { app }))}</p>
45
- <p style="margin: 0 0 24px;">${renderButton({ url: args.downloadUrl, label: t(locale, "gdpr.mail.exportReady.button") })}</p>
46
- <p style="margin: 0 0 8px; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }))}</p>
47
- ${renderFallbackUrl({ url: args.downloadUrl, label: t(locale, "gdpr.mail.fallbackUrl") })}`;
48
- const text = plainTextBody([
49
- t(locale, "gdpr.mail.greeting"),
50
- t(locale, "gdpr.mail.exportReady.intro", { app }),
51
- `${t(locale, "gdpr.mail.exportReady.button")}: ${args.downloadUrl}`,
52
- t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }),
53
- ]);
54
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
55
- }
56
- export function renderExportFailedEmail(args) {
57
- const locale = args.locale ?? "en";
58
- const app = appNameFor(args);
59
- const subject = t(locale, "gdpr.mail.exportFailed.subject", { app });
60
- const body = `
61
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
62
- <p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportFailed.intro", { app }))}</p>`;
63
- const text = plainTextBody([
64
- t(locale, "gdpr.mail.greeting"),
65
- t(locale, "gdpr.mail.exportFailed.intro", { app }),
66
- ]);
67
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
68
- }
69
- export function renderDeletionRequestedEmail(args) {
70
- const locale = args.locale ?? "en";
71
- const app = appNameFor(args);
72
- const when = formatTimestamp(args.gracePeriodEnd);
73
- const subject = t(locale, "gdpr.mail.deletionRequested.subject", { app });
74
- const body = `
75
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
76
- <p style="margin: 0 0 16px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.intro", { app, when }))}</p>
77
- <p style="margin: 0; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.cancel"))}</p>`;
78
- const text = plainTextBody([
79
- t(locale, "gdpr.mail.greeting"),
80
- t(locale, "gdpr.mail.deletionRequested.intro", { app, when }),
81
- t(locale, "gdpr.mail.deletionRequested.cancel"),
82
- ]);
83
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
84
- }
85
- export function renderDeletionExecutedEmail(args) {
86
- const locale = args.locale ?? "en";
87
- const app = appNameFor(args);
88
- const when = formatTimestamp(args.executedAt);
89
- const subject = t(locale, "gdpr.mail.deletionExecuted.subject", { app });
90
- const body = `
91
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
92
- <p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }))}</p>`;
93
- const text = plainTextBody([
94
- t(locale, "gdpr.mail.greeting"),
95
- t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }),
96
- ]);
97
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
98
- }
99
- function plainTextBody(paragraphs) {
100
- return paragraphs.join("\n\n");
101
- }
102
- function wrapCell(bodyHtml) {
103
- return `<tr><td>${bodyHtml}</td></tr>`;
104
- }
105
- function renderShell(args) {
106
- return `<!DOCTYPE html>
107
- <html lang="${escapeHtmlAttr(args.locale)}">
108
- <head>
109
- <meta charset="utf-8" />
110
- <meta name="viewport" content="width=device-width, initial-scale=1" />
111
- <title>${escapeHtml(args.title)}</title>
112
- </head>
113
- <body style="margin: 0; padding: 0; background: #f7f7f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a;">
114
- <table width="100%" cellpadding="0" cellspacing="0" style="padding: 24px 0;">
115
- <tr>
116
- <td align="center">
117
- <table width="560" cellpadding="0" cellspacing="0" style="max-width: 560px; background: #ffffff; border-radius: 8px; padding: 32px;">
118
- ${args.bodyHtml}
119
- </table>
120
- </td>
121
- </tr>
122
- </table>
123
- </body>
124
- </html>`;
125
- }
126
- function renderButton(args) {
127
- return `<a href="${escapeHtmlAttr(args.url)}" style="display: inline-block; background: #1a1a1a; color: #ffffff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500;">${escapeHtml(args.label)}</a>`;
128
- }
129
- function renderFallbackUrl(args) {
130
- return `<p style="margin: 24px 0 0; font-size: 12px; color: #666;">${escapeHtml(args.label)}<br /><a href="${escapeHtmlAttr(args.url)}" style="color: #1a1a1a; word-break: break-all;">${escapeHtml(args.url)}</a></p>`;
131
- }
132
- function formatTimestamp(iso) {
31
+ export function formatGdprMailTimestamp(iso) {
133
32
  try {
134
33
  const z = Temporal.Instant.from(iso).toZonedDateTimeISO("UTC");
135
34
  return `${z.year}-${pad2(z.month)}-${pad2(z.day)} ${pad2(z.hour)}:${pad2(z.minute)} UTC`;