@cosmicdrift/kumiko-samples 0.363.0 → 0.365.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 (237) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +1 -1
  3. package/packages/bundled-features/package.json +7 -1
  4. package/packages/bundled-features/src/audit/changes.json +18 -0
  5. package/packages/bundled-features/src/audit/feature.ts +1 -1
  6. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  7. package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
  8. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
  9. package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
  10. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
  12. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  13. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
  14. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
  15. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  17. package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
  18. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
  20. package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
  21. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  22. package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
  24. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  25. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
  26. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  27. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
  28. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  29. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  33. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  34. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
  35. package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
  36. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
  37. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  38. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
  39. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  40. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
  41. package/packages/bundled-features/src/cap-counter/changes.json +19 -0
  42. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  43. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
  44. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  45. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  46. package/packages/bundled-features/src/cap-overview/changes.json +6 -0
  47. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  48. package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
  49. package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
  50. package/packages/bundled-features/src/channel-email/changes.json +18 -0
  51. package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
  52. package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
  53. package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
  54. package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
  55. package/packages/bundled-features/src/config/changes.json +6 -0
  56. package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
  57. package/packages/bundled-features/src/config/resolver.ts +15 -12
  58. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  59. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  60. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
  61. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  62. package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
  63. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  64. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
  65. package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
  66. package/packages/bundled-features/src/delivery/changes.json +30 -0
  67. package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
  68. package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
  69. package/packages/bundled-features/src/delivery/index.ts +2 -0
  70. package/packages/bundled-features/src/delivery/jobs.ts +51 -14
  71. package/packages/bundled-features/src/delivery/types.ts +7 -1
  72. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  73. package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
  74. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
  75. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  76. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
  78. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  79. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  80. package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
  82. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
  83. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  84. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  85. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
  86. package/packages/bundled-features/src/jobs/changes.json +31 -0
  87. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  88. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  89. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  90. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  91. package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
  92. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  93. package/packages/bundled-features/src/ledger/changes.json +7 -0
  94. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  95. package/packages/bundled-features/src/metrics/changes.json +6 -0
  96. package/packages/bundled-features/src/metrics/filters.ts +28 -12
  97. package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
  98. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
  99. package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
  100. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  101. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  102. package/packages/bundled-features/src/secrets/changes.json +12 -0
  103. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  104. package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
  105. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  106. package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
  107. package/packages/bundled-features/src/shared/changes.json +6 -0
  108. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  109. package/packages/bundled-features/src/shared/index.ts +8 -0
  110. package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
  111. package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
  112. package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
  113. package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
  114. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  115. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  116. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  117. package/packages/bundled-features/src/template-resolver/README.md +4 -2
  118. package/packages/bundled-features/src/template-resolver/api.ts +3 -2
  119. package/packages/bundled-features/src/template-resolver/changes.json +149 -0
  120. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
  121. package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
  122. package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
  123. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
  124. package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
  125. package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
  126. package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
  127. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
  128. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
  129. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
  130. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
  131. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  132. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
  133. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
  134. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
  135. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
  136. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
  137. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
  138. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
  140. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
  141. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
  142. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
  143. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
  144. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
  145. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
  146. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
  147. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
  148. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
  149. package/packages/bundled-features/src/template-resolver/index.ts +18 -2
  150. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
  151. package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
  152. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  153. package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
  154. package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
  155. package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
  156. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
  157. package/packages/bundled-features/src/template-resolver/table.ts +13 -1
  158. package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
  159. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  160. package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
  161. package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
  162. package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
  163. package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
  164. package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
  165. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
  166. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
  167. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
  168. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
  169. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
  170. package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
  171. package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
  172. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
  173. package/packages/bundled-features/src/tenant/changes.json +6 -0
  174. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
  175. package/packages/bundled-features/src/tenant/seeding.ts +33 -27
  176. package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
  177. package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
  178. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
  179. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
  180. package/packages/bundled-features/src/tier-engine/changes.json +18 -0
  181. package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
  182. package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
  183. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  184. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
  185. package/packages/bundled-features/src/tier-engine/index.ts +2 -0
  186. package/packages/bundled-features/src/user/changes.json +6 -0
  187. package/packages/bundled-features/src/user/constants.ts +1 -0
  188. package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
  189. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  190. package/packages/bundled-features/src/user/seeding.ts +19 -5
  191. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  192. package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
  193. package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
  194. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
  195. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
  196. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  197. package/packages/bundled-features/src/waitlist/changes.json +18 -0
  198. package/packages/bundled-features/src/waitlist/feature.ts +1 -1
  199. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
  200. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
  201. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
  202. package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
  203. package/packages/bundled-features/src/waitlist/lib.ts +20 -0
  204. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  205. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  206. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  207. package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
  208. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  209. package/samples/apps/ui-walkthrough/package.json +9 -0
  210. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  211. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
  212. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
  213. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
  214. package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
  215. package/samples/apps/use-all-bundled/README.md +3 -13
  216. package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
  217. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
  218. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
  219. package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
  220. package/samples/recipes/auth-mfa/README.md +2 -1
  221. package/samples/recipes/auth-mfa/src/feature.ts +1 -0
  222. package/samples/recipes/custom-handlers/README.md +4 -3
  223. package/samples/recipes/custom-handlers/src/feature.ts +21 -12
  224. package/samples/recipes/dashboard-stats/README.md +38 -0
  225. package/samples/recipes/dashboard-stats/package.json +8 -0
  226. package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
  227. package/samples/recipes/legal-pages/README.md +2 -2
  228. package/samples/recipes/managed-pages/README.md +2 -2
  229. package/samples/recipes/pdf-audit-export/README.md +2 -1
  230. package/samples/recipes/pdf-audit-export/package.json +1 -1
  231. package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
  232. package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
  233. package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
  234. package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
  235. package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
  237. package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
@@ -1,5 +1,6 @@
1
1
  // @runtime client
2
2
  import type { ContentEditorProps } from "@cosmicdrift/kumiko-renderer";
3
+ import type { ReactNode } from "react";
3
4
 
4
5
  export type DocumentToolbarBlock =
5
6
  | "image"
@@ -8,7 +9,8 @@ export type DocumentToolbarBlock =
8
9
  | "divider"
9
10
  | "repeat"
10
11
  | "pageBreak"
11
- | "align";
12
+ | "align"
13
+ | "steps";
12
14
 
13
15
  /** Imperative access for an outside panel (placeholder list, resource list). */
14
16
  export type DocumentEditorHandle = {
@@ -39,8 +41,13 @@ export type DocumentEditorProps = ContentEditorProps & {
39
41
  readonly chrome?: DocumentEditorChrome;
40
42
  /** "compact" renders the placeholder and block buttons icon-only with a tooltip, for narrow columns. Default "full". */
41
43
  readonly toolbar?: "full" | "compact";
42
- /** Called with the handle once the editor exists and with null when it is destroyed. */
43
- readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
44
+ /** Canvas only: wraps the editable area in a caller-owned page (letterhead, margins) instead of the default mail paper. */
45
+ readonly renderPaper?: (editable: ReactNode) => ReactNode;
46
+ /** Canvas only: renders the toolbar (and its insert panel) into this element instead of above the editor;
47
+ * null renders none. Lets several region editors share one toolbar that follows the focused one. */
48
+ readonly toolbarHost?: HTMLElement | null;
49
+ /** Called with the handle once the editor exists and with null when it is destroyed (unmount or rebuild), so a held handle never outlives its editor. */
50
+ readonly onEditorReady?: (handle: DocumentEditorHandle | null) => void;
44
51
  };
45
52
 
46
53
  export const MAIL_DOCUMENT_BLOCKS = [
@@ -49,6 +56,7 @@ export const MAIL_DOCUMENT_BLOCKS = [
49
56
  "button",
50
57
  "divider",
51
58
  "repeat",
59
+ "steps",
52
60
  ] as const satisfies readonly DocumentToolbarBlock[];
53
61
 
54
62
  export const PDF_DOCUMENT_BLOCKS = [
@@ -57,5 +65,11 @@ export const PDF_DOCUMENT_BLOCKS = [
57
65
  "table",
58
66
  "divider",
59
67
  "repeat",
68
+ "steps",
60
69
  "pageBreak",
61
70
  ] as const satisfies readonly DocumentToolbarBlock[];
71
+
72
+ /** Block set for a template-resource kind; every kind except "document-pdf" gets the mail set. */
73
+ export function documentBlocksForKind(kind: string | undefined): readonly DocumentToolbarBlock[] {
74
+ return kind === "document-pdf" ? PDF_DOCUMENT_BLOCKS : MAIL_DOCUMENT_BLOCKS;
75
+ }
@@ -17,6 +17,7 @@ export const defaultTranslations: TranslationsByLocale = {
17
17
  "template-resolver.documentEditor.divider": "Divider",
18
18
  "template-resolver.documentEditor.heading3": "Heading 3",
19
19
  "template-resolver.documentEditor.image": "Image",
20
+ "template-resolver.documentEditor.imageBlock": "Image · inline",
20
21
  "template-resolver.documentEditor.imageAlt": "Alt text (optional)",
21
22
  "template-resolver.documentEditor.insert": "Insert",
22
23
  "template-resolver.documentEditor.insertPlaceholder": "Placeholder",
@@ -36,13 +37,19 @@ export const defaultTranslations: TranslationsByLocale = {
36
37
  "template-resolver.documentEditor.alignJustify": "Justified",
37
38
  "template-resolver.documentEditor.repeatBarList": "Repeat · {list}",
38
39
  "template-resolver.documentEditor.repeatHeader": "Repeat header row",
39
- "template-resolver.documentEditor.keepRowsTogether": "Keep rows together",
40
40
  "template-resolver.documentEditor.paragraph": "Paragraph",
41
41
  "template-resolver.documentEditor.paragraphStyle": "Paragraph style",
42
42
  "template-resolver.documentEditor.placeholder": "Write your text…",
43
43
  "template-resolver.documentEditor.removeLink": "Remove link",
44
44
  "template-resolver.documentEditor.repeat": "Repeat",
45
45
  "template-resolver.documentEditor.repeatList": "List to repeat",
46
+ "template-resolver.documentEditor.steps": "Steps",
47
+ "template-resolver.documentEditor.stepsVariable": "Current step from",
48
+ "template-resolver.documentEditor.stepsList": "Steps, separated by commas (value=label)",
49
+ "template-resolver.documentEditor.stepsDefault":
50
+ "reported=Reported, investigating=Investigating, resolved=Resolved",
51
+ "template-resolver.documentEditor.invalidSteps":
52
+ "Enter 1 to 8 different steps; values use letters, digits, - and _.",
46
53
  "template-resolver.documentEditor.resourceKey": "Resource key",
47
54
  "template-resolver.documentEditor.table": "Table",
48
55
  "template-resolver.editor.titleLabel": "Title",
@@ -82,11 +89,31 @@ export const defaultTranslations: TranslationsByLocale = {
82
89
  "templateResolver.errors.resource.unsupportedImage":
83
90
  "This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
84
91
  "templateResolver.errors.template.systemResourcesNeedSystemTenant":
85
- "Resource files can only be imported as system templates by a user of the system tenant.",
92
+ "Resource files of system templates can only be imported or exported by a user of the system tenant.",
93
+ "templateResolver.errors.template.bundleLocalesDiffer":
94
+ "The locales of this template differ in content format or variables, so they cannot be exported as one bundle.",
86
95
  "templateResolver.errors.collection.kindNotAllowed":
87
96
  "“{detail}” templates are written through their own editor, not through a collection.",
88
97
  "templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
89
- "templateResolver.errors.template.invalidMeta": "Invalid template settings: {detail}",
98
+ "templateResolver.errors.noDraft": "There is no draft to discard.",
99
+ "templateResolver.errors.template.invalidMeta": "The template settings are invalid.",
100
+ "templateResolver.errors.template.meta.subject.required": "The subject is missing.",
101
+ "templateResolver.errors.template.meta.subject.invalid":
102
+ "The subject is invalid (at most 200 characters, no line breaks).",
103
+ "templateResolver.errors.template.meta.previewText.required": "The preview text is missing.",
104
+ "templateResolver.errors.template.meta.previewText.invalid":
105
+ "The preview text is invalid (at most 200 characters, no line breaks).",
106
+ "templateResolver.errors.template.meta.senderName.required": "The sender name is missing.",
107
+ "templateResolver.errors.template.meta.senderName.invalid":
108
+ "The sender name is invalid (at most 80 characters, no line breaks).",
109
+ "templateResolver.errors.template.meta.replyTo.required": "The reply-to address is missing.",
110
+ "templateResolver.errors.template.meta.replyTo.invalid":
111
+ "The reply-to address is not a valid email address.",
112
+ "templateResolver.errors.template.meta.layoutSlug.required": "The layout is missing.",
113
+ "templateResolver.errors.template.meta.layoutSlug.invalid": "The chosen layout is invalid.",
114
+ "templateResolver.errors.template.meta.fileName.required": "The file name is missing.",
115
+ "templateResolver.errors.template.meta.fileName.invalid":
116
+ "The file name is invalid: no slashes, quotes, semicolons or two dots in a row.",
90
117
  "templateResolver.errors.template.unknownPlaceholders": "Unknown placeholders: {detail}",
91
118
  "templateResolver.errors.template.missingRequiredPlaceholders":
92
119
  "Required placeholders are not used: {detail}",
@@ -112,7 +139,8 @@ export const defaultTranslations: TranslationsByLocale = {
112
139
  "There is no PDF template “{slug}” for this tenant or the system.",
113
140
  "template-resolver.documentEditor.htmlSource": "HTML source",
114
141
  "template-resolver.documentEditor.removeBlock": "Remove block",
115
- "template-resolver.documentEditor.selectedBlock": "Selected block",
142
+ "template-resolver.documentEditor.list": "List",
143
+ "template-resolver.documentEditor.more": "More",
116
144
  "template-resolver.documentEditor.showHtml": "HTML",
117
145
  "template-resolver.documentEditor.underline": "Underline",
118
146
  "template-resolver.editor.saveFailed": "Save failed.",
@@ -5,6 +5,14 @@
5
5
  // `@cosmicdrift/kumiko-bundled-features/template-resolver` and has no React or
6
6
  // DOM deps.
7
7
 
8
+ export { MAIL_TONES, type MailTone } from "../mail-constants.js";
9
+ export {
10
+ PDF_DEFAULT_MARGIN_MM,
11
+ PDF_FOOTER_MARGIN_MM,
12
+ PDF_HEADER_MARGIN_MM,
13
+ PDF_ORIENTATIONS,
14
+ PDF_PAGE_FORMATS,
15
+ } from "../pdf-constants.js";
8
16
  export {
9
17
  TemplateResolverErrors,
10
18
  TemplateResolverHandlers,
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { cn } from "@cosmicdrift/kumiko-renderer-web";
8
8
  import { Extension, Node } from "@tiptap/core";
9
- import { UndoRedo } from "@tiptap/extensions";
9
+ import { Placeholder, UndoRedo } from "@tiptap/extensions";
10
10
  import { EditorContent, useEditor } from "@tiptap/react";
11
11
  import StarterKit from "@tiptap/starter-kit";
12
12
  import { type ReactNode, useMemo, useState } from "react";
@@ -26,7 +26,11 @@ export type PlaceholderLineEditorProps = {
26
26
  /** Placeholders shown as system chips (a different tint). */
27
27
  readonly systemVariables?: readonly string[];
28
28
  readonly readOnly?: boolean;
29
- readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
29
+ /** Drops the input frame so the line sits flat inside a label/value row. */
30
+ readonly flat?: boolean;
31
+ /** Hint shown while the line is empty. */
32
+ readonly placeholder?: string;
33
+ readonly onEditorReady?: (handle: DocumentEditorHandle | null) => void;
30
34
  readonly "aria-label": string;
31
35
  };
32
36
 
@@ -50,6 +54,8 @@ function LineEditorBody({
50
54
  variableLabels,
51
55
  systemVariables,
52
56
  readOnly,
57
+ flat,
58
+ placeholder,
53
59
  onEditorReady,
54
60
  ariaLabel,
55
61
  }: {
@@ -59,7 +65,9 @@ function LineEditorBody({
59
65
  readonly variableLabels: Readonly<Record<string, string>>;
60
66
  readonly systemVariables: readonly string[];
61
67
  readonly readOnly: boolean;
62
- readonly onEditorReady: ((handle: DocumentEditorHandle) => void) | undefined;
68
+ readonly flat: boolean;
69
+ readonly placeholder: string | undefined;
70
+ readonly onEditorReady: ((handle: DocumentEditorHandle | null) => void) | undefined;
63
71
  readonly ariaLabel: string;
64
72
  }): ReactNode {
65
73
  const extensions = useMemo(
@@ -88,12 +96,13 @@ function LineEditorBody({
88
96
  undoRedo: false,
89
97
  }),
90
98
  DocumentVariable.extend({
91
- addNodeView: () => createVariableView(variableLabels, systemVariables),
99
+ addNodeView: () => createVariableView(variableLabels, systemVariables, flat),
92
100
  }),
93
101
  UndoRedo,
94
102
  BlockLineBreaks,
103
+ ...(placeholder === undefined ? [] : [Placeholder.configure({ placeholder })]),
95
104
  ],
96
- [variableLabels, systemVariables],
105
+ [variableLabels, systemVariables, flat, placeholder],
97
106
  );
98
107
 
99
108
  const editor = useEditor({
@@ -111,6 +120,7 @@ function LineEditorBody({
111
120
  transformPastedHTML: (html) => singleLine(html.replace(/<[^>]*>/g, " ")),
112
121
  },
113
122
  onCreate: ({ editor: created }) => onEditorReady?.(createEditorHandle(created)),
123
+ onDestroy: () => onEditorReady?.(null),
114
124
  onUpdate: ({ editor: e }) => onChange(contentToLine(e.getJSON().content?.[0]?.content)),
115
125
  });
116
126
 
@@ -118,9 +128,11 @@ function LineEditorBody({
118
128
  <EditorContent
119
129
  editor={editor}
120
130
  className={cn(
121
- "flex min-h-9 items-center rounded-md border border-input bg-transparent px-3 text-base md:text-sm",
131
+ "flex items-center bg-transparent text-base md:text-sm",
132
+ flat ? "min-h-10" : "min-h-9 rounded-md border border-input px-3",
122
133
  "[&_.ProseMirror]:w-full [&_.ProseMirror]:whitespace-nowrap [&_.ProseMirror]:outline-none",
123
134
  "[&_p]:m-0",
135
+ "[&_.is-editor-empty]:before:pointer-events-none [&_.is-editor-empty]:before:float-left [&_.is-editor-empty]:before:h-0 [&_.is-editor-empty]:before:text-muted-foreground [&_.is-editor-empty]:before:content-[attr(data-placeholder)]",
124
136
  readOnly && "opacity-60",
125
137
  )}
126
138
  />
@@ -134,6 +146,8 @@ export default function PlaceholderLineEditor({
134
146
  variableLabels = NO_LABELS,
135
147
  systemVariables = NO_NAMES,
136
148
  readOnly = false,
149
+ flat = false,
150
+ placeholder,
137
151
  onEditorReady,
138
152
  "aria-label": ariaLabel,
139
153
  }: PlaceholderLineEditorProps): ReactNode {
@@ -152,7 +166,7 @@ export default function PlaceholderLineEditor({
152
166
 
153
167
  return (
154
168
  <LineEditorBody
155
- key={`${epoch}-${readOnly}-${JSON.stringify(stableLabels)}`}
169
+ key={`${epoch}-${readOnly}-${JSON.stringify(stableLabels)}-${stableSystem.join("\n")}`}
156
170
  id={id}
157
171
  initialValue={value}
158
172
  onChange={(next) => {
@@ -162,6 +176,8 @@ export default function PlaceholderLineEditor({
162
176
  variableLabels={stableLabels}
163
177
  systemVariables={stableSystem}
164
178
  readOnly={readOnly}
179
+ flat={flat}
180
+ placeholder={placeholder}
165
181
  onEditorReady={onEditorReady}
166
182
  ariaLabel={ariaLabel}
167
183
  />
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.365.0",
4
+ "type": "fix",
5
+ "title": "Post-save seed hooks use the after-commit notify factory",
6
+ "detail": "afterCommit seed hooks now get a notify function bound to the signup dispatcher's job runner when the dispatcher provides one, falling back to the regular factory. The afterCommit hook context is built only when the hook actually runs."
7
+ },
2
8
  {
3
9
  "version": "0.357.0",
4
10
  "type": "improvement",
@@ -9,7 +9,6 @@ import {
9
9
  mayOverrideTenant,
10
10
  mergeAssignedRoles,
11
11
  parseTenantId,
12
- type SessionUser,
13
12
  SYSTEM_ROLE,
14
13
  type TenantId,
15
14
  withResponseData,
@@ -50,10 +49,6 @@ function resolveTargetTenantId(
50
49
  return parseTenantId(payloadTenantId) ?? undefined;
51
50
  }
52
51
 
53
- function foreignStreamTenant(user: SessionUser, targetTenantId: TenantId): TenantId | undefined {
54
- return targetTenantId === user.tenantId ? undefined : targetTenantId;
55
- }
56
-
57
52
  // Editing your OWN roles keeps the current session: roles are re-derived per
58
53
  // request, so the new roles apply at once without a re-login. Other sessions
59
54
  // of that user and any foreign edit still revoke all.
@@ -195,8 +190,8 @@ export const updateMemberRolesWrite = defineWriteHandler({
195
190
  }
196
191
 
197
192
  // The membership stream lives on the membership's tenant (where seedTenantMembership/
198
- // invite-accept wrote it); a cross-tenant operator addresses it without becoming a member.
199
- // Same-tenant writes run on a tenant-mode db, which refuses a stream override.
193
+ // invite-accept wrote it); streamTenantId addresses it with the operator as actor, also
194
+ // without membership there.
200
195
  const result = await executor.update(
201
196
  {
202
197
  id: row["id"] as string, // @cast-boundary db-row
@@ -205,7 +200,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
205
200
  },
206
201
  event.user,
207
202
  db,
208
- { streamTenantId: foreignStreamTenant(event.user, targetTenantId) },
203
+ { streamTenantId: targetTenantId },
209
204
  );
210
205
 
211
206
  return withResponseData(result, {
@@ -101,26 +101,6 @@ export type SeedTenantHooks = {
101
101
  readonly context: AppContext;
102
102
  };
103
103
 
104
- // Shared by seedTenant/seedTenantMembership/auth-email-password's
105
- // seedUserWithPassword: fires the same entity-scoped postSave hooks the
106
- // real dispatcher would (registry.getEntityPostSaveHooks by entityName),
107
- // just without the full HTTP/JWT roundtrip a fixture doesn't need. Fresh
108
- // lifecycle-hooks instance per call, no systemHooks — only feature-
109
- // registered entity/handler hooks fire (search-index/SSE system hooks are
110
- // wired at server-boot and out of reach here).
111
- //
112
- // Tenant re-scope (#1566): when `targetTenantId` is set we rebuild the
113
- // tenant-bound pieces of the context (`_tenantId`, `db`, `notify`,
114
- // `hasFeature`, `user.tenantId`) — a shallow `{ ...ctx, _tenantId }` left
115
- // closures over the caller's tenant (self-signup is anonymous/system).
116
- // `query`/`write` bridges still share the caller's identity; hooks that
117
- // need a full identity switch use `writeAs`/`queryAs`.
118
- //
119
- // afterCommit (#1566): when `hooks.context.scheduleAfterCommit` is present
120
- // (HandlerContext from a live write), afterCommit hooks are queued onto the
121
- // dispatcher's sink and flush post-commit. Without a sink (fixture / plain
122
- // Connection) they still fire immediately — there is no outer TX to wait for.
123
- //
124
104
  function buildSeedHookNotify(
125
105
  context: AppContext,
126
106
  scopedUser: SessionUser,
@@ -129,7 +109,11 @@ function buildSeedHookNotify(
129
109
  ): AppContext["notify"] {
130
110
  const notifyFactory = context._notifyFactory;
131
111
  if (!notifyFactory) return context.notify;
132
- if (notifyTiming === "inline") return notifyFactory(scopedUser, targetTenantId);
112
+ const notifyAfterCommit = (context._notifyAfterCommitFactory ?? notifyFactory)(
113
+ scopedUser,
114
+ targetTenantId,
115
+ );
116
+ if (notifyTiming === "inline") return notifyAfterCommit;
133
117
  // Still inside the caller's write tx: delivery DB work must not wait for an app-pool slot.
134
118
  const notifyInsideTransaction = (context._notifyInTransactionFactory ?? notifyFactory)(
135
119
  scopedUser,
@@ -140,12 +124,31 @@ function buildSeedHookNotify(
140
124
  return createDeferringNotify({
141
125
  registry: context.registry,
142
126
  tenantId: targetTenantId,
143
- notifyAfterCommit: notifyFactory(scopedUser, targetTenantId),
127
+ notifyAfterCommit,
144
128
  notifyNow: notifyInsideTransaction,
145
129
  scheduleAfterCommit,
146
130
  });
147
131
  }
148
132
 
133
+ // Shared by seedTenant/seedTenantMembership/auth-email-password's
134
+ // seedUserWithPassword: fires the same entity-scoped postSave hooks the
135
+ // real dispatcher would (registry.getEntityPostSaveHooks by entityName),
136
+ // just without the full HTTP/JWT roundtrip a fixture doesn't need. Fresh
137
+ // lifecycle-hooks instance per call, no systemHooks — only feature-
138
+ // registered entity/handler hooks fire (search-index/SSE system hooks are
139
+ // wired at server-boot and out of reach here).
140
+ //
141
+ // Tenant re-scope (#1566): when `targetTenantId` is set we rebuild the
142
+ // tenant-bound pieces of the context (`_tenantId`, `db`, `notify`,
143
+ // `hasFeature`, `user.tenantId`) — a shallow `{ ...ctx, _tenantId }` left
144
+ // closures over the caller's tenant (self-signup is anonymous/system).
145
+ // `query`/`write` bridges still share the caller's identity; hooks that
146
+ // need a full identity switch use `writeAs`/`queryAs`.
147
+ //
148
+ // afterCommit (#1566): when `hooks.context.scheduleAfterCommit` is present
149
+ // (HandlerContext from a live write), afterCommit hooks are queued onto the
150
+ // dispatcher's sink and flush post-commit. Without a sink (fixture / plain
151
+ // Connection) they still fire immediately — there is no outer TX to wait for.
149
152
  // `runner` comes from the caller, which already holds a declared DbRunner —
150
153
  // a TenantDb exposes no runner back out to feature code.
151
154
  function scopeSeedHookContext(
@@ -219,13 +222,16 @@ export async function fireEntityPostSave(
219
222
  target === undefined
220
223
  ? hooks.context
221
224
  : scopeSeedHookContext(hooks.context, target.tenantId, target.db, "deferUntilCommit");
222
- const afterCommitContext =
223
- target === undefined
224
- ? hooks.context
225
- : scopeSeedHookContext(hooks.context, target.tenantId, target.db, "inline");
226
225
  await lifecycle.runPostSave(pseudoType, entityData, scopedContext, HookPhases.inTransaction);
227
226
  const runAfterCommit = () =>
228
- lifecycle.runPostSave(pseudoType, entityData, afterCommitContext, HookPhases.afterCommit);
227
+ lifecycle.runPostSave(
228
+ pseudoType,
229
+ entityData,
230
+ target === undefined
231
+ ? hooks.context
232
+ : scopeSeedHookContext(hooks.context, target.tenantId, target.db, "inline"),
233
+ HookPhases.afterCommit,
234
+ );
229
235
  if (scopedContext.scheduleAfterCommit) {
230
236
  scopedContext.scheduleAfterCommit(runAfterCommit);
231
237
  } else {
@@ -28,6 +28,7 @@ import {
28
28
  executeRawQuery,
29
29
  extractTableName,
30
30
  physicalColumnName,
31
+ toPostgresArrayLiteral,
31
32
  } from "@cosmicdrift/kumiko-framework/db";
32
33
  import { MAX_TRANSFER_DEPTH, type Registry } from "@cosmicdrift/kumiko-framework/engine";
33
34
  import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
@@ -79,7 +80,12 @@ async function moveFileRefs(args: {
79
80
  args.db,
80
81
  `UPDATE ${FILE_REFS_TABLE} SET tenant_id = $1 ` +
81
82
  `WHERE tenant_id = $2 AND entity_type = $3 AND entity_id = ANY($4) RETURNING id`,
82
- [args.destinationTenantId, args.sourceTenantId, args.entityType, args.entityIds],
83
+ [
84
+ args.destinationTenantId,
85
+ args.sourceTenantId,
86
+ args.entityType,
87
+ toPostgresArrayLiteral(args.entityIds),
88
+ ],
83
89
  );
84
90
  const movedIds = rows.map((row) => row.id);
85
91
  if (movedIds.length === 0) return 0;
@@ -126,7 +132,12 @@ async function moveChildRows(args: {
126
132
  `UPDATE "${args.tableName}" SET "${args.tenantCol}" = $1 ` +
127
133
  `WHERE "${args.typeCol}" = $2 AND "${args.idCol}" = ANY($3) AND "${args.tenantCol}" = $4 ` +
128
134
  `RETURNING "${args.pkCol}" AS id`,
129
- [args.destinationTenantId, args.parentEntityType, args.parentRowIds, args.sourceTenantId],
135
+ [
136
+ args.destinationTenantId,
137
+ args.parentEntityType,
138
+ toPostgresArrayLiteral(args.parentRowIds),
139
+ args.sourceTenantId,
140
+ ],
130
141
  );
131
142
  return rows.map((row) => row.id);
132
143
  }
@@ -149,7 +160,7 @@ async function moveReferencedChildRows(args: {
149
160
  `UPDATE "${args.tableName}" SET "${args.tenantCol}" = $1 ` +
150
161
  `WHERE "${args.refCol}" = ANY($2) AND "${args.tenantCol}" = $3 ` +
151
162
  `RETURNING "${args.pkCol}" AS id`,
152
- [args.destinationTenantId, args.parentRowIds, args.sourceTenantId],
163
+ [args.destinationTenantId, toPostgresArrayLiteral(args.parentRowIds), args.sourceTenantId],
153
164
  );
154
165
  return rows.map((row) => row.id);
155
166
  }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.365.0",
4
+ "type": "fix",
5
+ "title": "One sweep tick shares a single stage budget between all destroying tenants",
6
+ "detail": "The destruction sweep used to give every destroying tenant its own full stage budget, so a few large tenants stretched one tick far beyond the minute cron window and the following ticks were skipped. The tick budget is now divided evenly over the destroying tenants. Each tenant's slice starts when that tenant's turn begins, so an overrun by one tenant never leaves the next ones with a zero slice. runNextDestructionStage accepts an optional deadlineAt."
7
+ },
2
8
  {
3
9
  "version": "0.354.0",
4
10
  "type": "improvement",
@@ -290,6 +290,7 @@ export async function runNextDestructionStage(args: {
290
290
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
291
291
  readonly escapeHatchAuditLog?: Logger;
292
292
  readonly actor?: string;
293
+ readonly deadlineAt?: number;
293
294
  }): Promise<StageTickResult> {
294
295
  const events = await loadAggregate(args.db, args.tenantId, args.tenantId);
295
296
  const { completed, abandoned, attemptsByStage } = replayStageState(events);
@@ -316,7 +317,7 @@ export async function runNextDestructionStage(args: {
316
317
  escapeHatchAuditSink: args.escapeHatchAuditSink,
317
318
  escapeHatchAuditLog: args.escapeHatchAuditLog,
318
319
  actor: args.actor,
319
- deadlineAt: Date.now() + DESTRUCTION_STAGE_TICK_BUDGET_MS,
320
+ deadlineAt: args.deadlineAt ?? Date.now() + DESTRUCTION_STAGE_TICK_BUDGET_MS,
320
321
  };
321
322
 
322
323
  let version = lastEventVersion(events);
@@ -456,9 +457,16 @@ export async function runTenantDestructionSweep(args: {
456
457
 
457
458
  let advanced = 0;
458
459
  const destroying = await selectMany<{ id: string }>(args.db, tenantTable, destroyingWhere);
460
+ // One budget per sweep tick, split evenly so a large tenant cannot eat the
461
+ // window and starve the ones behind it. Each slice starts when its tenant
462
+ // starts: anchoring to a shared tick deadline would hand tenants after an
463
+ // overrun a zero slice, and a no-progress stage counts as a failed attempt.
464
+ const sliceMs = DESTRUCTION_STAGE_TICK_BUDGET_MS / Math.max(1, destroying.length);
459
465
  for (const row of destroying) {
466
+ const tenantDeadlineAt = Date.now() + sliceMs;
460
467
  try {
461
468
  const result = await runNextDestructionStage({
469
+ deadlineAt: tenantDeadlineAt,
462
470
  db: args.db,
463
471
  registry: args.registry,
464
472
  tenantId: row.id as TenantId,
@@ -88,8 +88,9 @@ const tenantMembershipCrud = createEventStoreExecutor(
88
88
  },
89
89
  );
90
90
 
91
- // Every hook still runs each tick (they are idempotent), so a later hook is not
92
- // starved by an earlier one that keeps reporting done:false.
91
+ // Every hook runs each tick (they are idempotent) but all share one ctx.deadlineAt: an
92
+ // earlier hook that keeps reporting done:false can use up the budget, so later chunking
93
+ // hooks time out with nothing processed until it finishes.
93
94
  function newOutcomeAccumulator(): {
94
95
  add: (result: TenantDestroyHookResult | void) => void;
95
96
  outcome: () => StageRunOutcome;
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.365.0",
4
+ "type": "improvement",
5
+ "title": "set-tenant-tier accepts an optional source so a manual grant can return to billing control",
6
+ "detail": "The handler payload takes source (manual, billing or default, default manual). Writing the row with source billing makes the subscription tier sync treat it as billing-driven again, so the next webhook overwrites the tier. The handler stays SystemAdmin only."
7
+ },
8
+ {
9
+ "version": "0.365.0",
10
+ "type": "fix",
11
+ "title": "tier-assignment source is a select field of TierAssignmentSources",
12
+ "detail": "The source column keeps its text storage, but the entity now validates it against manual, billing and default, so a typo can no longer bypass the manual-grant protection of the billing sync. TierAssignmentSource and isTierAssignmentSource are exported; the billing sync narrows the stored value with the guard."
13
+ },
14
+ {
15
+ "version": "0.364.0",
16
+ "type": "fix",
17
+ "title": "Tier cache: stale single reload cannot overwrite a newer full resync",
18
+ "detail": "A single-tenant reload that started before a full resync and resolved after it could restore an outdated tier. The resync now advances the per-tenant ticket for deleted and adopted tenants too, and assignment changes are published remote-only."
19
+ },
2
20
  {
3
21
  "version": "0.354.0",
4
22
  "type": "fix",
@@ -22,6 +22,15 @@ export const TierAssignmentSources = {
22
22
  default: "default",
23
23
  } as const;
24
24
 
25
+ export type TierAssignmentSource =
26
+ (typeof TierAssignmentSources)[keyof typeof TierAssignmentSources];
27
+
28
+ const TIER_ASSIGNMENT_SOURCE_VALUES: readonly string[] = Object.values(TierAssignmentSources);
29
+
30
+ export function isTierAssignmentSource(value: unknown): value is TierAssignmentSource {
31
+ return typeof value === "string" && TIER_ASSIGNMENT_SOURCE_VALUES.includes(value);
32
+ }
33
+
25
34
  // Qualified query handler names.
26
35
  export const TierEngineQueries = {
27
36
  list: "tier-engine:query:tier-assignment:list",
@@ -1,4 +1,9 @@
1
- import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
1
+ import {
2
+ createEntity,
3
+ createSelectField,
4
+ createTextField,
5
+ } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { type TierAssignmentSource, TierAssignmentSources } from "./constants.js";
2
7
 
3
8
  // tier-assignment — pro Plattform-Tenant genau ein Aggregat. Aggregate-ID
4
9
  // wird deterministisch aus tenantId abgeleitet (uuidv5, siehe aggregate-id.ts).
@@ -35,9 +40,9 @@ export const tierAssignmentEntity = createEntity({
35
40
  }),
36
41
  // Origin of the assignment, see TierAssignmentSources. Optional for back-compat with
37
42
  // rows without source. Protects manual grants from being overwritten by the billing sync.
38
- source: createTextField({
43
+ source: createSelectField({
44
+ options: Object.values(TierAssignmentSources),
39
45
  required: false,
40
- maxLength: 20,
41
46
  personal: false,
42
47
  reason: "technical_reference",
43
48
  }),
@@ -48,6 +53,6 @@ export type TierAssignmentRow = {
48
53
  readonly id: string;
49
54
  readonly version: number;
50
55
  readonly tier: string;
51
- readonly source: string | null;
56
+ readonly source: TierAssignmentSource | null;
52
57
  readonly tenantId: string;
53
58
  };
@@ -338,7 +338,8 @@ export function createTierEngineFeature<
338
338
  };
339
339
  const publishAssignmentChange = (tenantId: TenantId): void => {
340
340
  const message: TierAssignmentSyncMessage = { tenantId };
341
- busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message);
341
+ // The writer already updated its own cache; only other pods need to re-read.
342
+ busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message, { remoteOnly: true });
342
343
  };
343
344
 
344
345
  // Trial: zeit-abgeleitet aus tenant.inserted_at (≈ Signup), live am
@@ -533,10 +534,17 @@ export function createTierEngineFeature<
533
534
  const isNewerThanSnapshot = (tenantId: TenantId): boolean =>
534
535
  (latestTicket.get(tenantId) ?? 0) > startedAtTicket;
535
536
  for (const tenantId of [...cache.keys()]) {
536
- if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) cache.delete(tenantId);
537
+ if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) {
538
+ cache.delete(tenantId);
539
+ latestTicket.set(tenantId, startedAtTicket);
540
+ }
537
541
  }
538
542
  for (const [tenantId, features] of loaded) {
539
- if (!isNewerThanSnapshot(tenantId)) cache.set(tenantId, features);
543
+ if (!isNewerThanSnapshot(tenantId)) {
544
+ cache.set(tenantId, features);
545
+ // Makes a single reload that started before this snapshot discard its older result.
546
+ latestTicket.set(tenantId, startedAtTicket);
547
+ }
540
548
  }
541
549
  };
542
550