@cosmicdrift/kumiko-bundled-features 0.387.0 → 0.389.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 (320) hide show
  1. package/dist/agent-tools/index.d.ts +1 -1
  2. package/dist/agent-tools/index.js +1 -1
  3. package/dist/agent-tools/tool-dispatch.d.ts +12 -7
  4. package/dist/agent-tools/tool-dispatch.js +42 -31
  5. package/dist/audit/feature.js +1 -0
  6. package/dist/audit/handlers/audit-events.js +1 -1
  7. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
  8. package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
  9. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
  10. package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
  11. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
  12. package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
  13. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
  14. package/dist/auth-email-password/handlers/signup-request.write.js +4 -0
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
  16. package/dist/auth-email-password/handlers/token-request-handler.js +5 -0
  17. package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
  18. package/dist/auth-email-password/magic-link-mail.js +7 -3
  19. package/dist/auth-email-password/signup-activation.d.ts +1 -0
  20. package/dist/auth-email-password/signup-activation.js +1 -0
  21. package/dist/auth-email-password/web/auth-client.d.ts +2 -2
  22. package/dist/auth-email-password/web/auth-client.js +8 -4
  23. package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
  24. package/dist/auth-email-password/web/auth-redirect.js +20 -25
  25. package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
  26. package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
  27. package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
  28. package/dist/auth-email-password/web/index.d.ts +1 -0
  29. package/dist/auth-email-password/web/index.js +1 -0
  30. package/dist/auth-email-password/web/login-screen.js +11 -6
  31. package/dist/auth-email-password/web/reset-password-screen.js +7 -1
  32. package/dist/auth-email-password/web/return-path.d.ts +4 -0
  33. package/dist/auth-email-password/web/return-path.js +34 -0
  34. package/dist/auth-email-password/web/session.js +6 -1
  35. package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
  36. package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
  37. package/dist/auth-email-password/web/signup-screen.js +8 -3
  38. package/dist/auth-mfa/config.d.ts +1 -1
  39. package/dist/auth-mfa/config.js +3 -7
  40. package/dist/auth-mfa/feature-exports.d.ts +2 -2
  41. package/dist/auth-mfa/feature.js +8 -0
  42. package/dist/auth-mfa/mfa-status-checker.js +5 -3
  43. package/dist/auth-mfa/web/i18n.js +5 -1
  44. package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
  45. package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
  46. package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
  47. package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
  48. package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
  49. package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
  50. package/dist/billing-foundation/checkout-core.d.ts +9 -14
  51. package/dist/billing-foundation/checkout-core.js +24 -39
  52. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
  53. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
  54. package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
  55. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
  56. package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
  57. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
  58. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
  59. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
  60. package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
  61. package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
  62. package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
  63. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  64. package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
  65. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
  66. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
  67. package/dist/billing-foundation/events.d.ts +1 -0
  68. package/dist/billing-foundation/events.js +1 -0
  69. package/dist/billing-foundation/feature.js +3 -1
  70. package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
  71. package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
  72. package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
  73. package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
  74. package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
  75. package/dist/billing-foundation/handlers/process-event.write.js +8 -2
  76. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
  77. package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
  78. package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
  79. package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
  80. package/dist/billing-foundation/i18n.js +22 -0
  81. package/dist/billing-foundation/index.d.ts +1 -1
  82. package/dist/billing-foundation/plan-catalog.d.ts +21 -5
  83. package/dist/billing-foundation/plan-catalog.js +25 -16
  84. package/dist/billing-foundation/plan-switch.d.ts +12 -0
  85. package/dist/billing-foundation/plan-switch.js +60 -0
  86. package/dist/billing-foundation/types.d.ts +57 -1
  87. package/dist/billing-foundation/types.js +9 -0
  88. package/dist/billing-foundation/validate-options.js +17 -0
  89. package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
  90. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
  91. package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
  92. package/dist/billing-foundation/web/constants.d.ts +4 -0
  93. package/dist/billing-foundation/web/constants.js +4 -0
  94. package/dist/billing-foundation/web/plan-price.d.ts +15 -0
  95. package/dist/billing-foundation/web/plan-price.js +32 -0
  96. package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
  97. package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
  98. package/dist/cap-counter/cap-reservation.js +20 -7
  99. package/dist/cap-counter/enforce-cap.js +2 -1
  100. package/dist/cap-counter/feature.js +1 -0
  101. package/dist/cap-counter/stock-cap-guard.js +7 -1
  102. package/dist/cap-overview/feature.js +1 -0
  103. package/dist/channel-email/feature.js +1 -1
  104. package/dist/config/handlers/cascade.query.d.ts +5 -0
  105. package/dist/config/handlers/cascade.query.js +4 -3
  106. package/dist/config/handlers/set.write.js +12 -2
  107. package/dist/config/handlers/values.query.d.ts +5 -0
  108. package/dist/config/handlers/values.query.js +4 -3
  109. package/dist/config/write-helpers.d.ts +1 -0
  110. package/dist/config/write-helpers.js +18 -9
  111. package/dist/crypto-shredding/handlers/forget-subject.write.js +14 -11
  112. package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
  113. package/dist/delivery/channel-context.js +6 -3
  114. package/dist/delivery/db/queries/preferences.js +2 -2
  115. package/dist/delivery/delivery-service.js +22 -7
  116. package/dist/delivery/feature.js +1 -0
  117. package/dist/delivery/handlers/preferences.query.d.ts +1 -1
  118. package/dist/delivery/handlers/preferences.query.js +20 -1
  119. package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
  120. package/dist/delivery/index.d.ts +1 -0
  121. package/dist/delivery/index.js +1 -0
  122. package/dist/delivery/system-channel-context.d.ts +15 -0
  123. package/dist/delivery/system-channel-context.js +5 -0
  124. package/dist/delivery/unsubscribe-link.d.ts +3 -1
  125. package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
  126. package/dist/delivery/unsubscribe-page-strings.js +1 -0
  127. package/dist/delivery/unsubscribe-page.d.ts +2 -0
  128. package/dist/delivery/unsubscribe-page.js +5 -0
  129. package/dist/delivery/unsubscribe.js +25 -7
  130. package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
  131. package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
  132. package/dist/document-ingest-foundation/events.d.ts +11 -0
  133. package/dist/document-ingest-foundation/events.js +11 -0
  134. package/dist/document-ingest-foundation/index.d.ts +2 -1
  135. package/dist/document-ingest-foundation/index.js +2 -1
  136. package/dist/feature-toggles/feature.js +1 -0
  137. package/dist/file-foundation/index.d.ts +1 -1
  138. package/dist/file-foundation/index.js +1 -1
  139. package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
  140. package/dist/file-foundation/read-file-ref-content.js +6 -1
  141. package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
  142. package/dist/form-draft/db/queries/cleanup.js +2 -2
  143. package/dist/form-draft/decrypt-draft.js +5 -1
  144. package/dist/host-routing/dispatch.js +11 -20
  145. package/dist/host-routing/validate-options.js +3 -0
  146. package/dist/inbound-mail-foundation/feature.js +1 -0
  147. package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
  148. package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
  149. package/dist/inbound-mail-foundation/i18n.js +0 -1
  150. package/dist/inbound-mail-foundation/screens.js +0 -3
  151. package/dist/inbound-mail-foundation/types.d.ts +4 -3
  152. package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
  153. package/dist/jobs/feature.d.ts +1 -0
  154. package/dist/jobs/feature.js +9 -4
  155. package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
  156. package/dist/jobs/handlers/tenant-failures.query.js +45 -41
  157. package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
  158. package/dist/jobs/handlers/tenant-runs.query.js +54 -50
  159. package/dist/jobs/job-run-logger.js +5 -4
  160. package/dist/ledger/web/index.js +1 -1
  161. package/dist/legal-pages/seed.d.ts +3 -1
  162. package/dist/legal-pages/seed.js +2 -2
  163. package/dist/mail-foundation/feature.d.ts +3 -3
  164. package/dist/mail-foundation/feature.js +3 -0
  165. package/dist/mail-transport-smtp/config-keys.js +1 -1
  166. package/dist/mail-transport-smtp/feature.js +6 -5
  167. package/dist/managed-pages/constants.d.ts +1 -0
  168. package/dist/managed-pages/constants.js +1 -0
  169. package/dist/managed-pages/feature.js +3 -1
  170. package/dist/managed-pages/index.d.ts +1 -0
  171. package/dist/managed-pages/index.js +1 -0
  172. package/dist/notes-history/handlers/add-note.write.js +1 -1
  173. package/dist/personal-access-tokens/feature.d.ts +1 -1
  174. package/dist/personal-access-tokens/feature.js +5 -0
  175. package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
  176. package/dist/renderer-pdf/render-table-pdf.js +22 -10
  177. package/dist/secrets/write-gate.js +8 -2
  178. package/dist/sessions/feature.d.ts +2 -2
  179. package/dist/sessions/feature.js +5 -0
  180. package/dist/sessions/index.d.ts +1 -1
  181. package/dist/sessions/index.js +1 -1
  182. package/dist/sessions/session-callbacks.d.ts +0 -2
  183. package/dist/sessions/session-callbacks.js +1 -3
  184. package/dist/shared/decrypt-stored-pii.d.ts +1 -0
  185. package/dist/shared/decrypt-stored-pii.js +6 -1
  186. package/dist/shared/index.d.ts +1 -1
  187. package/dist/shared/index.js +1 -1
  188. package/dist/shared/mail-template-body.d.ts +6 -2
  189. package/dist/shared/mail-template-body.js +7 -4
  190. package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
  191. package/dist/shared/purge-files-reaching-subject.js +19 -2
  192. package/dist/shared/run-in-sub-transaction.js +5 -5
  193. package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
  194. package/dist/step-dispatcher/feature.js +1 -0
  195. package/dist/step-dispatcher/mail-runner.d.ts +1 -0
  196. package/dist/subscription-dodo/client.d.ts +17 -0
  197. package/dist/subscription-dodo/client.js +31 -0
  198. package/dist/subscription-dodo/constants.d.ts +12 -0
  199. package/dist/subscription-dodo/constants.js +19 -0
  200. package/dist/subscription-dodo/feature.d.ts +37 -0
  201. package/dist/subscription-dodo/feature.js +144 -0
  202. package/dist/subscription-dodo/index.d.ts +2 -0
  203. package/dist/subscription-dodo/index.js +4 -0
  204. package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
  205. package/dist/subscription-dodo/plugin-methods.js +260 -0
  206. package/dist/subscription-dodo/runtime.d.ts +27 -0
  207. package/dist/subscription-dodo/runtime.js +89 -0
  208. package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
  209. package/dist/subscription-dodo/verify-webhook.js +189 -0
  210. package/dist/subscription-stripe/feature.js +1 -0
  211. package/dist/subscription-stripe/runtime.js +7 -6
  212. package/dist/tags/feature.js +1 -0
  213. package/dist/template-resolver/api.d.ts +3 -2
  214. package/dist/template-resolver/api.js +24 -2
  215. package/dist/template-resolver/compile-document-body.js +1 -0
  216. package/dist/template-resolver/document-ops.d.ts +5 -0
  217. package/dist/template-resolver/document-ops.js +18 -2
  218. package/dist/template-resolver/feature.js +4 -4
  219. package/dist/template-resolver/handlers/bundle-import.js +3 -3
  220. package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
  221. package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
  222. package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
  223. package/dist/template-resolver/handlers/document-base.js +3 -2
  224. package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
  225. package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
  226. package/dist/template-resolver/handlers/layout-source.query.js +26 -6
  227. package/dist/template-resolver/handlers/list.query.js +5 -6
  228. package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
  229. package/dist/template-resolver/handlers/overview.query.js +21 -10
  230. package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
  231. package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
  232. package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
  233. package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
  234. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
  235. package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
  236. package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
  237. package/dist/template-resolver/handlers/usage.query.js +26 -16
  238. package/dist/template-resolver/handlers/validate.query.js +7 -2
  239. package/dist/template-resolver/seeding.d.ts +1 -0
  240. package/dist/template-resolver/seeding.js +31 -12
  241. package/dist/template-resolver/store-template-resource.d.ts +1 -0
  242. package/dist/template-resolver/store-template-resource.js +65 -10
  243. package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
  244. package/dist/template-resolver/template-delivery-policy.js +0 -6
  245. package/dist/template-resolver/template-meta.js +7 -4
  246. package/dist/template-resolver/template-preview.js +4 -3
  247. package/dist/template-resolver/web/constants.d.ts +2 -2
  248. package/dist/template-resolver/web/constants.js +3 -3
  249. package/dist/template-resolver/web/i18n.js +3 -0
  250. package/dist/template-resolver/web/index.d.ts +1 -0
  251. package/dist/template-resolver/web/index.js +1 -0
  252. package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
  253. package/dist/template-resolver/web/mail-layout-constants.js +7 -0
  254. package/dist/template-resolver/web/qualified-names.d.ts +1 -0
  255. package/dist/template-resolver/web/qualified-names.js +1 -0
  256. package/dist/tenant/feature.js +1 -0
  257. package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
  258. package/dist/tenant/handlers/member-directory.query.js +10 -3
  259. package/dist/tenant/handlers/team-list.query.js +3 -3
  260. package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
  261. package/dist/tenant/handlers/tenant-directory.query.js +8 -4
  262. package/dist/tenant-lifecycle/constants.d.ts +1 -0
  263. package/dist/tenant-lifecycle/constants.js +4 -0
  264. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
  265. package/dist/tenant-lifecycle/stages.js +22 -1
  266. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
  267. package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
  268. package/dist/tier-engine/web/constants.js +2 -2
  269. package/dist/user/feature.js +1 -0
  270. package/dist/user-data-rights/feature.js +1 -0
  271. package/dist/user-data-rights/index.d.ts +1 -1
  272. package/dist/user-data-rights/index.js +1 -1
  273. package/dist/user-data-rights/run-export-jobs.js +3 -4
  274. package/dist/user-data-rights/run-forget-cleanup.js +2 -1
  275. package/dist/user-data-rights/web/constants.d.ts +6 -0
  276. package/dist/user-data-rights/web/constants.js +6 -0
  277. package/dist/user-data-rights/web/index.d.ts +1 -0
  278. package/dist/user-data-rights/web/index.js +1 -0
  279. package/dist/waitlist/feature.js +1 -0
  280. package/dist/waitlist/handlers/export-purge.write.js +2 -1
  281. package/dist/waitlist/handlers/submit.write.js +14 -10
  282. package/dist/waitlist/import.d.ts +3 -1
  283. package/dist/waitlist/import.js +26 -2
  284. package/dist/waitlist/legacy-migrate.js +18 -7
  285. package/dist/waitlist/link-on-signup.js +18 -6
  286. package/dist/waitlist/options.d.ts +5 -1
  287. package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
  288. package/package.json +19 -9
  289. package/src/agent-tools/changes.json +12 -0
  290. package/src/audit/changes.json +12 -0
  291. package/src/auth-email-password/changes.json +12 -0
  292. package/src/auth-mfa/changes.json +12 -0
  293. package/src/billing-foundation/changes.json +55 -0
  294. package/src/cap-counter/changes.json +30 -0
  295. package/src/channel-email/changes.json +6 -0
  296. package/src/config/changes.json +18 -0
  297. package/src/crypto-shredding/changes.json +18 -0
  298. package/src/delivery/changes.json +42 -0
  299. package/src/document-ingest-foundation/changes.json +6 -0
  300. package/src/form-draft/changes.json +6 -0
  301. package/src/host-routing/changes.json +12 -0
  302. package/src/inbound-mail-foundation/changes.json +18 -0
  303. package/src/jobs/changes.json +13 -0
  304. package/src/mail-foundation/changes.json +6 -0
  305. package/src/mail-transport-smtp/changes.json +6 -0
  306. package/src/managed-pages/changes.json +6 -0
  307. package/src/renderer-pdf/changes.json +6 -0
  308. package/src/secrets/changes.json +6 -0
  309. package/src/sessions/changes.json +7 -0
  310. package/src/step-dispatcher/changes.json +12 -0
  311. package/src/subscription-dodo/changes.json +14 -0
  312. package/src/subscription-stripe/changes.json +12 -0
  313. package/src/template-resolver/changes.json +84 -0
  314. package/src/tenant/changes.json +6 -0
  315. package/src/tenant-lifecycle/changes.json +12 -0
  316. package/src/tier-engine/changes.json +6 -0
  317. package/src/user-data-rights/changes.json +12 -0
  318. package/src/waitlist/changes.json +36 -0
  319. package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
  320. package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
@@ -1,5 +1,5 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
- import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { defineQueryHandler, MAX_LIST_LIMIT, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import * as z from "zod";
4
4
  import { TenantQueries } from "../../tenant/index.js";
5
5
  import { templateResourcesTable } from "../table.js";
@@ -10,8 +10,31 @@ import { loadEnabledTenantIds, pickBestRowPerTenant, TENANT_OVERRIDES_REASON, }
10
10
  const directorySchema = z.object({
11
11
  rows: z.array(z.object({ id: z.string(), label: z.string() })),
12
12
  });
13
- function labelsOf(raw) {
14
- return new Map(directorySchema.parse(raw).rows.map((row) => [row.id, row.label]));
13
+ function chunksOf(items, size) {
14
+ const chunks = [];
15
+ for (let start = 0; start < items.length; start += size) {
16
+ chunks.push(items.slice(start, start + size));
17
+ }
18
+ return chunks;
19
+ }
20
+ // Looks up only the ids the overrides need, so labels never depend on the directory's default cap.
21
+ async function labelsFor(ctx, queryName, ids) {
22
+ const labels = new Map();
23
+ for (const chunk of chunksOf([...ids], MAX_LIST_LIMIT)) {
24
+ const raw = await ctx.query(queryName, { ids: chunk });
25
+ for (const row of directorySchema.parse(raw).rows)
26
+ labels.set(row.id, row.label);
27
+ }
28
+ return labels;
29
+ }
30
+ function changerIdsOf(rows) {
31
+ const ids = new Set();
32
+ for (const row of rows) {
33
+ const changedById = row.modifiedById ?? row.insertedById;
34
+ if (changedById !== null)
35
+ ids.add(changedById);
36
+ }
37
+ return ids;
15
38
  }
16
39
  function statusOf(row) {
17
40
  return row.status === "active" ? "active" : "draft";
@@ -55,10 +78,10 @@ export const tenantOverridesQuery = defineQueryHandler({
55
78
  const bestByTenant = pickBestRowPerTenant(found, enabledTenantIds);
56
79
  const best = [...bestByTenant.values()];
57
80
  const tenantLabels = hasTenantFeature
58
- ? labelsOf(await ctx.query(TenantQueries.tenantDirectory, {}))
81
+ ? await labelsFor(ctx, TenantQueries.tenantDirectory, new Set(best.map((row) => row.tenantId)))
59
82
  : new Map();
60
83
  const userLabels = hasTenantFeature
61
- ? labelsOf(await ctx.query(TenantQueries.memberDirectory, {}))
84
+ ? await labelsFor(ctx, TenantQueries.memberDirectory, changerIdsOf(best))
62
85
  : new Map();
63
86
  const rows = best
64
87
  .map((row) => toOverrideRow(row, tenantLabels, userLabels))
@@ -1,5 +1,6 @@
1
1
  import * as z from "zod";
2
- export declare const usageQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"usage", z.ZodObject<{
2
+ import type { TemplateEditPolicy } from "../template-edit-policy.ts";
3
+ export declare function makeUsageQuery(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"usage", z.ZodObject<{
3
4
  slug: z.ZodString;
4
5
  kind: z.ZodEnum<{
5
6
  "ai-prompt": "ai-prompt";
@@ -6,20 +6,30 @@ import { deliveryPolicyOf } from "../template-delivery-policy.js";
6
6
  import { templateSlugSchema } from "../template-meta.js";
7
7
  import { collectTemplateUsage } from "../template-usage.js";
8
8
  import { TEMPLATE_KINDS } from "../web/constants.js";
9
- import { effectiveMailLayoutSlugs } from "./overview.query.js";
9
+ import { effectiveMailLayoutSlugs, isRowVisibleToCaller } from "./overview.query.js";
10
10
  const MAIL_ROW_LIMIT = 5000;
11
- export const usageQuery = defineQueryHandler({
12
- name: "usage",
13
- schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
14
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
15
- description: "Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug and that has a code template, a mail layout by every mail notification whose effective template (own override, else system) names it, or whose code meta does when no stored template exists, or that falls back to the default layout; use it to show what a change to a template affects.",
16
- handler: async (query, ctx) => {
17
- const { slug, kind } = query.payload;
18
- if (kind !== "mail-layout")
19
- return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
20
- const mailRows = await selectMany(ctx.db, templateResourcesTable, { kind: "mail" }, { limit: MAIL_ROW_LIMIT });
21
- return {
22
- usedBy: collectTemplateUsage(ctx.registry, slug, kind, effectiveMailLayoutSlugs(mailRows, deliveryPolicyOf(ctx.registry))),
23
- };
24
- },
25
- });
11
+ export function makeUsageQuery(policy) {
12
+ return defineQueryHandler({
13
+ name: "usage",
14
+ schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
15
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
16
+ description: "Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug and that has a code template, a mail layout by every mail notification whose effective template (own override, else system) names it, or whose code meta does when no stored template exists, or that falls back to the default layout; use it to show what a change to a template affects.",
17
+ handler: async (query, ctx) => {
18
+ const { slug, kind } = query.payload;
19
+ if (kind !== "mail-layout")
20
+ return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
21
+ const fetchedRows = await selectMany(ctx.db, templateResourcesTable, { kind: "mail" }, { orderBy: { col: "id", direction: "asc" }, limit: MAIL_ROW_LIMIT + 1 });
22
+ if (fetchedRows.length > MAIL_ROW_LIMIT) {
23
+ ctx.log?.warn("template usage: row limit reached, result is incomplete", {
24
+ limit: MAIL_ROW_LIMIT,
25
+ });
26
+ }
27
+ const visibleRows = fetchedRows
28
+ .slice(0, MAIL_ROW_LIMIT)
29
+ .filter((row) => isRowVisibleToCaller(policy, query.user, ctx.registry, row));
30
+ return {
31
+ usedBy: collectTemplateUsage(ctx.registry, slug, kind, effectiveMailLayoutSlugs(visibleRows, deliveryPolicyOf(ctx.registry))),
32
+ };
33
+ },
34
+ });
35
+ }
@@ -1,9 +1,10 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineQueryHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import * as z from "zod";
4
+ import { isRowDeliveredTo } from "../api.js";
4
5
  import { imageResourceKeysOf } from "../linked-resources.js";
5
6
  import { templateResourcesTable } from "../table.js";
6
- import { deliveryPolicyOf, isRowServedTo } from "../template-delivery-policy.js";
7
+ import { deliveryPolicyOf } from "../template-delivery-policy.js";
7
8
  import { isSystemAdmin } from "../template-draft.js";
8
9
  import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
9
10
  import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
@@ -31,7 +32,11 @@ export async function knownResourceKeysFor(ctx, user, where) {
31
32
  locale,
32
33
  })),
33
34
  ...(scope === "tenant" &&
34
- isRowServedTo(deliveryPolicyOf(ctx.registry), user, { tenantId: user.tenantId, kind, slug })
35
+ (await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, {
36
+ tenantId: user.tenantId,
37
+ kind,
38
+ slug,
39
+ }))
35
40
  ? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
36
41
  : []),
37
42
  ];
@@ -64,4 +64,5 @@ export type LegalContentBlock = {
64
64
  export declare function seedLegalContentFromJson(db: DbConnection, blocks: readonly LegalContentBlock[], opts?: {
65
65
  readonly tenantId?: TenantId;
66
66
  readonly by?: SessionUser;
67
+ readonly folder?: string;
67
68
  }): Promise<void>;
@@ -39,10 +39,16 @@ async function resolveExistingForEventStoreSeed(db, tdb, existingRow, tenantId)
39
39
  }
40
40
  return { id, version: streamVersion };
41
41
  }
42
+ // Namespace prefix plus path, no whitespace: keeps free text from passing as a key that the UI
43
+ // would then show raw.
44
+ const I18N_KEY_FORMAT = /^[^\s:]+:\S+$/;
42
45
  function seedTitleColumn(opts, label) {
43
46
  if (opts.title !== undefined && opts.titleI18nKey !== undefined) {
44
47
  throw new Error(`${label} rejected: set either title or titleI18nKey, not both`);
45
48
  }
49
+ if (opts.titleI18nKey !== undefined && !I18N_KEY_FORMAT.test(opts.titleI18nKey)) {
50
+ throw new Error(`${label} rejected: titleI18nKey "${opts.titleI18nKey}" is not a "namespace:key" i18n key`);
51
+ }
46
52
  return opts.titleI18nKey ?? opts.title ?? null;
47
53
  }
48
54
  export async function seedSystemTemplate(db, opts) {
@@ -101,16 +107,18 @@ export async function seedSystemTemplate(db, opts) {
101
107
  current?.title === rowFields.title &&
102
108
  current?.variableSchema === rowFields.variableSchema &&
103
109
  current?.meta === rowFields.meta &&
104
- current?.status === rowFields.status &&
105
110
  current?.parentTemplateId === rowFields.parentTemplateId &&
106
111
  (linkedResources === undefined || current?.linkedResources === linkedResources)) {
107
112
  return { id: String(row.id) };
108
113
  }
109
- const result = await executor.update({
110
- id: row.id,
111
- version: row.version,
112
- changes: { ...rowFields, ...activeStampChanges(row) },
113
- }, by, tdb);
114
+ // An archived system row is a SystemAdmin decision (reversible with publish-system) that a
115
+ // pod-boot re-seed must not undo; content still follows the code. The seed replaces the
116
+ // content, so an open draft on the old content is dropped with it.
117
+ const { status: _status, ...contentFields } = rowFields;
118
+ const changes = current?.status === "archived"
119
+ ? { ...contentFields, status: "archived", draft: null }
120
+ : { ...rowFields, ...activeStampChanges(row), draft: null };
121
+ const result = await executor.update({ id: row.id, version: row.version, changes }, by, tdb);
114
122
  if (!result.isSuccess) {
115
123
  throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
116
124
  }
@@ -191,8 +199,10 @@ async function seedSystemTemplateBundleWithResources(db, bundle, opts) {
191
199
  if (!dispatcher) {
192
200
  throw new Error(`${label}: bundles with resources need a file provider; pass opts.dispatcher (the dispatcher of the seed deps) so the files are stored through the import-bundle-system write`);
193
201
  }
194
- if ((opts.ifExists ?? "skip") === "skip") {
195
- const existingIds = {};
202
+ const skipExisting = (opts.ifExists ?? "skip") === "skip";
203
+ const existingIds = {};
204
+ let bundleToImport = bundle;
205
+ if (skipExisting) {
196
206
  for (const locale of Object.keys(bundle.locales)) {
197
207
  const row = await fetchOne(db, templateResourcesTable, {
198
208
  tenantId: SYSTEM_TENANT_ID,
@@ -206,12 +216,18 @@ async function seedSystemTemplateBundleWithResources(db, bundle, opts) {
206
216
  if (Object.keys(existingIds).length === Object.keys(bundle.locales).length) {
207
217
  return { ids: existingIds };
208
218
  }
219
+ // import-bundle-system always overwrites, so only the missing locales go in: a
220
+ // hand-edited system row of an already seeded locale must survive the new-locale boot.
221
+ bundleToImport = {
222
+ ...bundle,
223
+ locales: Object.fromEntries(Object.entries(bundle.locales).filter(([locale]) => !(locale in existingIds))),
224
+ };
209
225
  }
210
- const result = await dispatcher.write(TemplateResolverHandlers.importBundleSystem, { bundle }, opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]));
226
+ const result = await dispatcher.write(TemplateResolverHandlers.importBundleSystem, { bundle: bundleToImport }, opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]));
211
227
  if (!result.isSuccess) {
212
228
  throw new Error(`${label} failed: ${result.error.code}: ${result.error.message}`);
213
229
  }
214
- return { ids: importedIds(result.data) };
230
+ return { ids: { ...existingIds, ...importedIds(result.data) } };
215
231
  }
216
232
  function importedIds(data) {
217
233
  const ids = isPlainObject(data) ? data["ids"] : undefined;
@@ -279,11 +295,13 @@ export async function seedTextBlock(db, opts) {
279
295
  status: "active",
280
296
  };
281
297
  const applyUpdate = async (row, current) => {
298
+ // No folder option keeps the row's folder, so a re-seed cannot drop it from the content tree.
299
+ const folder = opts.folder === undefined ? (current?.folder ?? null) : fields.folder;
282
300
  // Skip no-op updates so a legal re-seed on every pod boot does not bump
283
301
  // version/modifiedAt (ETag, "last changed", event-store growth).
284
302
  if (current?.title === fields.title &&
285
303
  current?.content === fields.content &&
286
- current?.folder === fields.folder) {
304
+ current?.folder === folder) {
287
305
  return { id: String(row.id) };
288
306
  }
289
307
  const result = await executor.update({
@@ -292,7 +310,7 @@ export async function seedTextBlock(db, opts) {
292
310
  changes: {
293
311
  title: fields.title,
294
312
  content: fields.content,
295
- folder: fields.folder,
313
+ folder,
296
314
  // A re-seed must not revive an archived block; only a served one gets a new served version.
297
315
  ...(current?.status === "active" && servedStampChanges(row)),
298
316
  },
@@ -353,6 +371,7 @@ export async function seedLegalContentFromJson(db, blocks, opts = {}) {
353
371
  locale: block.locale,
354
372
  title: block.title,
355
373
  content: block.content,
374
+ ...(opts.folder !== undefined && { folder: opts.folder }),
356
375
  ifExists: "update",
357
376
  by,
358
377
  });
@@ -42,6 +42,7 @@ export declare function prepareTemplateResourceBeforeTransaction(args: {
42
42
  }): Promise<BeforeTransactionResult<PreparedTemplateImage>>;
43
43
  export type Release = () => Promise<void>;
44
44
  export declare function releaseAll(releases: readonly Release[]): Promise<void>;
45
+ export declare function releaseAfterFailure(ctx: HandlerContext, releases: readonly Release[]): Promise<void>;
45
46
  export type PreparedLinkedResourceCopy = {
46
47
  readonly key: string;
47
48
  readonly entry: LinkedResourceEntry;
@@ -1,14 +1,13 @@
1
1
  import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { NotFoundError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import { createFileRefFromUploadedBytes, uploadFileBytesBeforeTransaction, } from "@cosmicdrift/kumiko-framework/files";
4
- import { fileContextForTenant } from "../file-foundation/index.js";
4
+ import { fileContextForTenant, readFileRefContent } from "../file-foundation/index.js";
5
5
  import { extensionForResourceContentType, requireForTenant } from "../shared/index.js";
6
6
  import { DEFAULT_RESOURCE_MAX_BYTES, MAX_FILE_ATTACHMENTS_PER_TEMPLATE, RESOURCE_MAX_BYTES_CONFIG_KEY, } from "./handlers/resource-shared.js";
7
7
  import { executor } from "./handlers/shared.js";
8
- import { attachmentEntriesOf, parseLinkedResourceEntries, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
8
+ import { attachmentEntriesOf, isFileAttachmentEntry, parseLinkedResourceEntries, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
9
9
  import { normalizeResourceImage } from "./resource-image.js";
10
10
  import { templateResourcesTable } from "./table.js";
11
- import { readTemplateResource } from "./template-resources.js";
12
11
  import { ACTIVE_TEMPLATE_STATUS, SYSTEM_TENANT_ID } from "./web/constants.js";
13
12
  import { TemplateResolverErrors } from "./web/qualified-names.js";
14
13
  export async function readTemplateResourceMaxBytes(config, registry) {
@@ -24,6 +23,9 @@ function keyInUse() {
24
23
  details: { code: "key_in_use" },
25
24
  }));
26
25
  }
26
+ function isImageKey(links, key) {
27
+ return typeof links[key] === "string";
28
+ }
27
29
  function isAttachmentKey(links, key) {
28
30
  const occupant = links[key];
29
31
  return occupant !== undefined && typeof occupant !== "string";
@@ -129,6 +131,49 @@ export async function releaseAll(releases) {
129
131
  if (rejected?.status === "rejected")
130
132
  throw rejected.reason;
131
133
  }
134
+ // Failure paths: a failing release must not replace the upload error the caller needs to see.
135
+ export async function releaseAfterFailure(ctx, releases) {
136
+ try {
137
+ await releaseAll(releases);
138
+ }
139
+ catch (releaseError) {
140
+ ctx.log?.warn("template upload rollback: releasing stored files failed", {
141
+ error: releaseError instanceof Error ? releaseError.message : String(releaseError),
142
+ });
143
+ }
144
+ }
145
+ // The source row's own fileName is personal data and may be ciphertext, so the copy is named from
146
+ // the link entry or the key.
147
+ function copiedFileName(key, entry, contentType) {
148
+ if (typeof entry !== "string" && isFileAttachmentEntry(entry))
149
+ return entry.fileName;
150
+ return `${key}.${extensionForResourceContentType(contentType) ?? "bin"}`;
151
+ }
152
+ function sourceUnreadable() {
153
+ return writeFailure(new UnprocessableError(TemplateResolverErrors.resourceSourceUnreadable, {
154
+ i18nKey: "templateResolver.errors.resource.sourceUnreadable",
155
+ }));
156
+ }
157
+ // Undefined: the fileRef row exists but its bytes cannot be read, which must not look like a
158
+ // deleted file. The provider is chosen by the tenant of the fileRef, not by the caller's.
159
+ async function readCopySource(ctx, fileRefId) {
160
+ try {
161
+ const found = await readFileRefContent({
162
+ db: ctx.db,
163
+ filesForTenant: (tid) => fileContextForTenant(ctx, ctx.user.tenantId, tid),
164
+ fileRefId,
165
+ allowedTenantIds: [SYSTEM_TENANT_ID, ctx.user.tenantId],
166
+ });
167
+ return found === null ? null : { contentType: found.contentType, bytes: found.content };
168
+ }
169
+ catch (cause) {
170
+ ctx.log?.warn("template copy: source file could not be read", {
171
+ fileRefId,
172
+ errorClass: cause instanceof Error ? cause.name : typeof cause,
173
+ });
174
+ return undefined;
175
+ }
176
+ }
132
177
  // beforeTransaction half: copies the file behind every entry into the target tenant's storage, so a
133
178
  // template copied from the system default keeps working once the tenant exports it or measures its
134
179
  // attachments. An entry whose source file is gone is left out: it could not be resolved at render
@@ -143,12 +188,21 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args) {
143
188
  copies.push({ key, entry });
144
189
  continue;
145
190
  }
146
- const file = await readTemplateResource(ctx, typeof entry === "string" ? entry : entry.fileRefId);
147
- if (file === null)
191
+ const source = await readCopySource(ctx, typeof entry === "string" ? entry : entry.fileRefId);
192
+ if (source === undefined) {
193
+ await releaseAfterFailure(ctx, releases);
194
+ return sourceUnreadable();
195
+ }
196
+ // skip: the source fileRef is deleted or unknown, so there is nothing to copy
197
+ if (source === null)
148
198
  continue;
149
- const uploaded = await uploadToTenant({ ctx, user, tenantId }, { fileName: file.fileName, mimeType: file.contentType, data: file.bytes });
199
+ const uploaded = await uploadToTenant({ ctx, user, tenantId }, {
200
+ fileName: copiedFileName(key, entry, source.contentType),
201
+ mimeType: source.contentType,
202
+ data: source.bytes,
203
+ });
150
204
  if (!uploaded.isSuccess) {
151
- await releaseAll(releases);
205
+ await releaseAfterFailure(ctx, releases);
152
206
  return uploaded;
153
207
  }
154
208
  if (uploaded.release)
@@ -157,7 +211,7 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args) {
157
211
  }
158
212
  }
159
213
  catch (cause) {
160
- await releaseAll(releases);
214
+ await releaseAfterFailure(ctx, releases);
161
215
  throw cause;
162
216
  }
163
217
  return { isSuccess: true, data: copies, release: () => releaseAll(releases) };
@@ -275,8 +329,7 @@ async function checkAttachmentTarget(args) {
275
329
  if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
276
330
  return attachmentLimitFailure();
277
331
  }
278
- const occupant = linked[key];
279
- if (occupant !== undefined && typeof occupant === "string")
332
+ if (isImageKey(linked, key))
280
333
  return keyInUse();
281
334
  return { isSuccess: true, data: { row, linked } };
282
335
  }
@@ -387,6 +440,8 @@ export async function linkTemplatePdfAttachment(args) {
387
440
  if (row.kind !== "mail")
388
441
  return attachmentNeedsMailTemplate();
389
442
  const linked = parseLinkedResourceEntries(row.linkedResources);
443
+ if (isImageKey(linked, key))
444
+ return keyInUse();
390
445
  if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
391
446
  return attachmentLimitFailure();
392
447
  }
@@ -13,10 +13,5 @@ export declare function isServedByCollectionOnly(policy: TemplateDeliveryPolicy,
13
13
  export declare function isRowServed(policy: TemplateDeliveryPolicy, row: DeliveryTarget & {
14
14
  readonly tenantId: string;
15
15
  }): boolean;
16
- export declare function isRowServedTo(policy: TemplateDeliveryPolicy, user: {
17
- readonly roles: readonly string[];
18
- }, row: DeliveryTarget & {
19
- readonly tenantId: string;
20
- }): boolean;
21
16
  export declare function deliveryPolicyOf(registry: Registry): TemplateDeliveryPolicy;
22
17
  export {};
@@ -1,5 +1,4 @@
1
1
  import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
- import { isSystemAdmin } from "./template-draft.js";
3
2
  import { SYSTEM_TENANT_ID } from "./web/constants.js";
4
3
  import { TEMPLATE_RESOLVER_FEATURE } from "./web/qualified-names.js";
5
4
  const NOTHING_UNLOCKED = { tenantEditable: [], collectionKinds: [] };
@@ -16,11 +15,6 @@ export function isServedByCollectionOnly(policy, target) {
16
15
  export function isRowServed(policy, row) {
17
16
  return row.tenantId === SYSTEM_TENANT_ID || isTenantOverrideServed(policy, row);
18
17
  }
19
- // A SystemAdmin sees every stored row, so a locked override can still be reset; everyone else
20
- // sees only what is delivered, which keeps a locked override out of the template-resolver queries for a TenantAdmin (the audit log keeps the history).
21
- export function isRowServedTo(policy, user, row) {
22
- return isSystemAdmin(user) || isRowServed(policy, row);
23
- }
24
18
  function isDeliveryTarget(value) {
25
19
  return (typeof propertyOf(value, "kind") === "string" && typeof propertyOf(value, "slug") === "string");
26
20
  }
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { HEX_COLOR_PATTERN, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./web/constants.js";
3
3
  import { MAIL_TONES } from "./web/mail-constants.js";
4
+ import { MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./web/mail-layout-constants.js";
4
5
  // One or more kebab segments joined by ":" — notification templates are seeded
5
6
  // under their qualified name `feature:notify:name`.
6
7
  export const templateSlugSchema = z
@@ -66,12 +67,14 @@ const pdfMetaSchema = z
66
67
  .optional(),
67
68
  })
68
69
  .strict();
69
- const MAIL_LAYOUT_FONTS = ["sans", "serif"];
70
- const MAIL_LAYOUT_ACCENT_LINES = ["branding", "none"];
71
- const MAIL_LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"];
70
+ const MAIL_LAYOUT_WIDTH_LITERALS = [
71
+ z.literal(MAIL_LAYOUT_WIDTHS[0]),
72
+ z.literal(MAIL_LAYOUT_WIDTHS[1]),
73
+ z.literal(MAIL_LAYOUT_WIDTHS[2]),
74
+ ];
72
75
  const mailLayoutMetaSchema = z
73
76
  .object({
74
- width: z.union([z.literal(560), z.literal(600), z.literal(640)]).optional(),
77
+ width: z.union(MAIL_LAYOUT_WIDTH_LITERALS).optional(),
75
78
  font: z.enum(MAIL_LAYOUT_FONTS).optional(),
76
79
  outerBackground: z.string().regex(HEX_COLOR_PATTERN, "must be a #rrggbb color").optional(),
77
80
  accentLine: z.enum(MAIL_LAYOUT_ACCENT_LINES).optional(),
@@ -8,7 +8,7 @@ import * as z from "zod";
8
8
  import { createEmailAttachmentReader, createTemplateMailRenderer } from "../delivery/index.js";
9
9
  import { fileContextForTenant } from "../file-foundation/index.js";
10
10
  import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
11
- import { createTemplateResolverApi, TemplateNotFoundError } from "./api.js";
11
+ import { createTemplateResolverApi, isRowDeliveredTo, TemplateNotFoundError, } from "./api.js";
12
12
  import { collectDocumentResourceKeys } from "./compile-document-body.js";
13
13
  import { parseStoredJson } from "./handlers/template-gates.js";
14
14
  import { validateTemplateDraft } from "./handlers/validate.query.js";
@@ -16,7 +16,7 @@ import { readLayoutRegions, sanitizeLayoutHtml, } from "./layout-regions.js";
16
16
  import { parseLinkedResourceEntries } from "./linked-resources.js";
17
17
  import { buildDisplayHtml } from "./preview-display-html.js";
18
18
  import { templateResourcesTable } from "./table.js";
19
- import { deliveryPolicyOf, isRowServedTo } from "./template-delivery-policy.js";
19
+ import { deliveryPolicyOf } from "./template-delivery-policy.js";
20
20
  import { isMailTemplateMeta, parseTemplateMeta, readPdfTemplateMeta, } from "./template-meta.js";
21
21
  import { DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, MAIL_PREVIEW_MAX_BYTES, SYSTEM_TENANT_ID, } from "./web/constants.js";
22
22
  import { TemplateResolverErrors } from "./web/qualified-names.js";
@@ -149,7 +149,8 @@ export function estimateMailBytes(args) {
149
149
  async function loadEditedRowResources(ctx, user, input) {
150
150
  const rows = await selectMany(ctx.db, templateResourcesTable, { tenantId: user.tenantId, slug: input.slug, kind: "mail", locale: input.locale }, { limit: 1 });
151
151
  const row = rows[0];
152
- if (row !== undefined && !isRowServedTo(deliveryPolicyOf(ctx.registry), user, row)) {
152
+ if (row !== undefined &&
153
+ !(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, row))) {
153
154
  return undefined;
154
155
  }
155
156
  return row === undefined ? undefined : parseLinkedResourceEntries(row.linkedResources);
@@ -10,7 +10,7 @@ export declare const UPSERT_KINDS: readonly ["mail", "document-pdf", "image-snap
10
10
  export type UpsertKind = (typeof UPSERT_KINDS)[number];
11
11
  export declare const TEXT_BLOCK_KIND: "text-block";
12
12
  export declare const COLLECTION_REFUSED_KINDS: readonly ["mail", "mail-layout", "pdf-layout"];
13
- export declare function isCollectionRefusedKind(kind: string): boolean;
13
+ export declare function isCollectionRefusedKind(kind: unknown): kind is (typeof COLLECTION_REFUSED_KINDS)[number];
14
14
  export declare const CONTENT_FORMATS: readonly ["markdown", "mjml", "html", "plain"];
15
15
  export type ContentFormat = (typeof CONTENT_FORMATS)[number];
16
16
  export declare const TEMPLATE_SCOPES: readonly ["system", "tenant"];
@@ -18,8 +18,8 @@ export type TemplateScope = (typeof TEMPLATE_SCOPES)[number];
18
18
  export declare const TEMPLATE_STATUSES: readonly ["draft", "active", "archived"];
19
19
  export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
20
20
  export declare const ACTIVE_TEMPLATE_STATUS = "active";
21
+ export { HEX_COLOR_PATTERN } from "./mail-layout-constants.ts";
21
22
  export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, } from "./pdf-constants.ts";
22
- export declare const HEX_COLOR_PATTERN: RegExp;
23
23
  export declare const DEFAULT_MAIL_LAYOUT_SLUG = "default";
24
24
  export declare const DEFAULT_PDF_LAYOUT_SLUG = "default";
25
25
  export declare const MAIL_PREVIEW_MAX_BYTES: number;
@@ -4,6 +4,7 @@
4
4
  // RenderKind identifies the consumer class of a template. Plugin renderers in
5
5
  // `renderer-foundation` match on kind; the resolver here is kind-agnostic: it
6
6
  // only loads, and the content format (markdown/mjml/html) decides who renders it.
7
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
7
8
  export const RENDER_KINDS = ["mail", "document-pdf", "image-snapshot"];
8
9
  // Kinds whose html body is a rich document: the server rebuilds it through the
9
10
  // document schema on every write (see document-sanitizer.ts).
@@ -44,15 +45,14 @@ export const COLLECTION_REFUSED_KINDS = [
44
45
  "pdf-layout",
45
46
  ];
46
47
  export function isCollectionRefusedKind(kind) {
47
- return COLLECTION_REFUSED_KINDS.some((refused) => refused === kind);
48
+ return isOneOf(COLLECTION_REFUSED_KINDS, kind);
48
49
  }
49
50
  export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"];
50
51
  export const TEMPLATE_SCOPES = ["system", "tenant"];
51
52
  export const TEMPLATE_STATUSES = ["draft", "active", "archived"];
52
53
  export const ACTIVE_TEMPLATE_STATUS = "active";
54
+ export { HEX_COLOR_PATTERN } from "./mail-layout-constants.js";
53
55
  export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.js";
54
- // Colors end up inside MJML attributes, so only a strict hex is ever written there.
55
- export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
56
56
  export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
57
57
  export const DEFAULT_PDF_LAYOUT_SLUG = "default";
58
58
  // The mail transport enforces no size limit of its own; 10 MiB is a common provider ceiling.
@@ -72,6 +72,8 @@ export const defaultTranslations = {
72
72
  "templateResolver.errors.patch.blockRemoved": "The block {blockId} was already removed by an earlier step.",
73
73
  "templateResolver.errors.patch.textNotFound": "The text {find} was not found in the block.",
74
74
  "templateResolver.errors.patch.textAmbiguous": "The text {find} occurs {count} times in the block; use a longer text.",
75
+ "templateResolver.errors.patch.textSpansFormatting": "The text {find} crosses a formatting change in the block; replace each formatted part on its own.",
76
+ "templateResolver.errors.patch.placeholderNotReplaceable": "The placeholder {find} cannot be replaced as text; set the text around it with another operation or remove the block.",
75
77
  "templateResolver.errors.patch.blockNotAllowed": "The block type {type} is not available in {kind} templates.",
76
78
  "templateResolver.errors.patch.blockTypeMismatch": "This step needs a {expected} block, but the block is a {actual}.",
77
79
  "templateResolver.errors.patch.invalidResourceKey": "The image key {resource} is not valid.",
@@ -86,6 +88,7 @@ export const defaultTranslations = {
86
88
  "templateResolver.errors.testMail.noAddress": "Your account has no email address to send the test mail to.",
87
89
  "templateResolver.errors.testMail.failed": "The test mail could not be sent.",
88
90
  "templateResolver.errors.resource.tooLarge": "The image is too large.",
91
+ "templateResolver.errors.resource.sourceUnreadable": "A file of the source template could not be read, so nothing was copied.",
89
92
  "templateResolver.errors.resource.unsupportedImage": "This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
90
93
  "templateResolver.errors.template.systemResourcesNeedSystemTenant": "Resource files of system templates can only be imported or exported by a user of the system tenant.",
91
94
  "templateResolver.errors.template.bundleLocalesDiffer": "The locales of this template differ in content format or variables, so they cannot be exported as one bundle.",
@@ -2,6 +2,7 @@ export { textBlocksClient } from "./client-plugin.tsx";
2
2
  export { DocumentContentEditor } from "./document-editor.tsx";
3
3
  export { type DocumentEditorChrome, type DocumentEditorHandle, type DocumentEditorProps, type DocumentResourcePreview, type DocumentToolbarBlock, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.ts";
4
4
  export { MAIL_TONES, type MailTone } from "./mail-constants.ts";
5
+ export { HEX_COLOR_PATTERN, MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./mail-layout-constants.ts";
5
6
  export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.ts";
6
7
  export { PlaceholderLineEditor, type PlaceholderLineEditorProps, } from "./placeholder-line-editor-lazy.tsx";
7
8
  export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.ts";
@@ -7,6 +7,7 @@ export { textBlocksClient } from "./client-plugin.js";
7
7
  export { DocumentContentEditor } from "./document-editor.js";
8
8
  export { MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.js";
9
9
  export { MAIL_TONES } from "./mail-constants.js";
10
+ export { HEX_COLOR_PATTERN, MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./mail-layout-constants.js";
10
11
  export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.js";
11
12
  export { PlaceholderLineEditor, } from "./placeholder-line-editor-lazy.js";
12
13
  export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.js";
@@ -0,0 +1,5 @@
1
+ export declare const MAIL_LAYOUT_WIDTHS: readonly [560, 600, 640];
2
+ export declare const MAIL_LAYOUT_FONTS: readonly ["sans", "serif"];
3
+ export declare const MAIL_LAYOUT_ACCENT_LINES: readonly ["branding", "none"];
4
+ export declare const MAIL_LAYOUT_BUTTON_STYLES: readonly ["filled-rounded", "filled-square", "outline"];
5
+ export declare const HEX_COLOR_PATTERN: RegExp;
@@ -0,0 +1,7 @@
1
+ // No imports: web/ re-exports these, and template-meta.ts pulls in zod and the framework engine.
2
+ export const MAIL_LAYOUT_WIDTHS = [560, 600, 640];
3
+ export const MAIL_LAYOUT_FONTS = ["sans", "serif"];
4
+ export const MAIL_LAYOUT_ACCENT_LINES = ["branding", "none"];
5
+ export const MAIL_LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"];
6
+ // Colors end up inside MJML attributes, so only a strict hex is ever written there.
7
+ export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
@@ -53,6 +53,7 @@ export declare const TemplateResolverErrors: {
53
53
  readonly bundleLocalesDiffer: "bundle_locales_differ";
54
54
  readonly resourceTooLarge: "template_resource_too_large";
55
55
  readonly resourceUnsupportedImage: "template_resource_unsupported_image";
56
+ readonly resourceSourceUnreadable: "template_resource_source_unreadable";
56
57
  readonly attachmentUnsupportedType: "template_attachment_unsupported_type";
57
58
  readonly attachmentLimit: "template_attachment_limit";
58
59
  readonly attachmentInvalid: "template_attachment_invalid";
@@ -63,6 +63,7 @@ export const TemplateResolverErrors = {
63
63
  bundleLocalesDiffer: "bundle_locales_differ",
64
64
  resourceTooLarge: "template_resource_too_large",
65
65
  resourceUnsupportedImage: "template_resource_unsupported_image",
66
+ resourceSourceUnreadable: "template_resource_source_unreadable",
66
67
  attachmentUnsupportedType: "template_attachment_unsupported_type",
67
68
  attachmentLimit: "template_attachment_limit",
68
69
  attachmentInvalid: "template_attachment_invalid",
@@ -31,6 +31,7 @@ export function collectAssignableAppRoles(features) {
31
31
  // --- Feature ---
32
32
  export function createTenantFeature(options) {
33
33
  return defineFeature("tenant", (r) => {
34
+ r.adoptsBundledLists();
34
35
  r.describe("Registers the three core multi-tenancy entities \u2014 `tenant`, `tenant-membership`, and `tenant-invitation` (DB tables `read_tenants`, `read_tenant_memberships`, and `read_tenant_invitations`) \u2014 along with write handlers for create/update/disable/enable/addMember/removeMember/updateMemberRoles and the matching queries. It also declares a set of per-tenant config keys (companyName, timezone, locale, SMTP credentials) and system-only keys (priceModel, maxUsers) via `r.config({ keys: { ... } })`. Use this feature in every multi-tenant app; membership resolution and invitation flows depend on it, and `auth-email-password` requires it.");
35
36
  r.uiHints({
36
37
  displayLabel: "Multi-Tenant Core",
@@ -2,6 +2,7 @@ import * as z from "zod";
2
2
  export declare const memberDirectoryQuery: import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"memberDirectory", z.ZodObject<{
3
3
  limit: z.ZodDefault<z.ZodNumber>;
4
4
  search: z.ZodOptional<z.ZodString>;
5
+ ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
5
6
  }, z.core.$strip>, {
6
7
  id: string;
7
8
  label: string;