@cosmicdrift/kumiko-samples 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 (274) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +8 -1
  3. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
  6. package/packages/bundled-features/src/audit/changes.json +12 -0
  7. package/packages/bundled-features/src/audit/feature.ts +1 -0
  8. package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
  10. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  11. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  12. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  13. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
  15. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  16. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  17. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  18. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  19. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  20. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  21. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  22. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  23. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  24. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  25. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  26. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  27. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  28. package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
  29. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  30. package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
  32. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
  33. package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
  34. package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
  35. package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
  36. package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
  37. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
  39. package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
  40. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
  51. package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
  52. package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
  53. package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
  55. package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
  57. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
  58. package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
  59. package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
  60. package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
  61. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  62. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
  63. package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
  64. package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
  65. package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
  66. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
  67. package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
  68. package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
  69. package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
  70. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
  71. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
  72. package/packages/bundled-features/src/cap-counter/changes.json +30 -0
  73. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
  74. package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
  75. package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
  76. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  77. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  78. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  79. package/packages/bundled-features/src/config/changes.json +18 -0
  80. package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
  81. package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
  82. package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
  83. package/packages/bundled-features/src/config/write-helpers.ts +27 -8
  84. package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
  85. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
  86. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  87. package/packages/bundled-features/src/delivery/changes.json +42 -0
  88. package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
  89. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
  90. package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
  91. package/packages/bundled-features/src/delivery/feature.ts +1 -0
  92. package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
  93. package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
  94. package/packages/bundled-features/src/delivery/index.ts +5 -0
  95. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  96. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
  97. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
  98. package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
  99. package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
  100. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  101. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  102. package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
  103. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  104. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  105. package/packages/bundled-features/src/file-foundation/index.ts +1 -0
  106. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
  107. package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
  108. package/packages/bundled-features/src/files/README.md +1 -1
  109. package/packages/bundled-features/src/form-draft/changes.json +6 -0
  110. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
  111. package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
  112. package/packages/bundled-features/src/host-routing/README.md +1 -1
  113. package/packages/bundled-features/src/host-routing/changes.json +12 -0
  114. package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
  115. package/packages/bundled-features/src/host-routing/options.ts +2 -1
  116. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
  118. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
  120. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
  122. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
  123. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
  124. package/packages/bundled-features/src/jobs/changes.json +13 -0
  125. package/packages/bundled-features/src/jobs/feature.ts +15 -4
  126. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
  127. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
  128. package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
  129. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  130. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  131. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  132. package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
  133. package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
  134. package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
  135. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  136. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
  137. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  138. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  139. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  140. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  141. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  142. package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
  143. package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
  144. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
  145. package/packages/bundled-features/src/secrets/changes.json +6 -0
  146. package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
  147. package/packages/bundled-features/src/sessions/changes.json +7 -0
  148. package/packages/bundled-features/src/sessions/feature.ts +8 -2
  149. package/packages/bundled-features/src/sessions/index.ts +1 -1
  150. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  151. package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
  152. package/packages/bundled-features/src/shared/index.ts +1 -1
  153. package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
  154. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
  155. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  156. package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
  157. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  158. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
  159. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  160. package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
  161. package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
  162. package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
  163. package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
  164. package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
  165. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
  166. package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
  167. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
  168. package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
  169. package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
  170. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
  171. package/packages/bundled-features/src/tags/feature.ts +1 -0
  172. package/packages/bundled-features/src/template-resolver/api.ts +31 -4
  173. package/packages/bundled-features/src/template-resolver/changes.json +84 -0
  174. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
  175. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  176. package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
  177. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
  178. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
  179. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
  180. package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
  181. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
  182. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  183. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
  184. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
  185. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  186. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  187. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
  188. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
  189. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
  190. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
  191. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
  192. package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
  193. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
  194. package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
  195. package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
  196. package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
  197. package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
  198. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  199. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  200. package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
  201. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  202. package/packages/bundled-features/src/tenant/changes.json +6 -0
  203. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  204. package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
  205. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  206. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
  207. package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
  208. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
  209. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
  210. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
  211. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  212. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
  213. package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
  214. package/packages/bundled-features/src/user/feature.ts +1 -0
  215. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  216. package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
  217. package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
  218. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
  219. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
  220. package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
  221. package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
  222. package/packages/bundled-features/src/waitlist/changes.json +36 -0
  223. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  224. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
  225. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
  226. package/packages/bundled-features/src/waitlist/import.ts +28 -3
  227. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
  228. package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
  229. package/packages/bundled-features/src/waitlist/options.ts +5 -1
  230. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
  231. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  232. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  233. package/samples/apps/styleguide/src/app/server.ts +4 -1
  234. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  235. package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
  236. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
  237. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  238. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  239. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  240. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  241. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  242. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  243. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  244. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
  245. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
  246. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
  247. package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
  248. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  249. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  250. package/samples/apps/use-all-bundled/package.json +2 -1
  251. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  252. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  253. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  254. package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
  255. package/samples/recipes/auth-mfa/README.md +5 -0
  256. package/samples/recipes/auth-mfa/package.json +5 -2
  257. package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
  258. package/samples/recipes/auth-mfa/tsconfig.json +8 -0
  259. package/samples/recipes/env-composition/README.md +3 -1
  260. package/samples/recipes/env-composition/src/env.ts +2 -0
  261. package/samples/recipes/golden-path/src/feature.ts +2 -1
  262. package/samples/recipes/legal-pages/README.md +1 -1
  263. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  264. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  265. package/samples/recipes/list-table-options/README.md +50 -0
  266. package/samples/recipes/list-table-options/package.json +11 -0
  267. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  268. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  269. package/samples/recipes/rate-limiting/README.md +18 -0
  270. package/samples/recipes/rate-limiting/src/feature.ts +12 -0
  271. package/samples/recipes/state-machine/src/events.ts +3 -2
  272. package/samples/recipes/tenant-isolation/README.md +1 -0
  273. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  274. package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
@@ -32,63 +32,67 @@ function parseSubject(stored: string | null): Record<string, unknown> | null {
32
32
  return Object.fromEntries(parsed.filter(isSubjectEntry));
33
33
  }
34
34
 
35
- export const tenantRunsQuery = defineQueryHandler({
36
- name: "tenant-runs",
37
- description:
38
- "Lists the calling tenant's own job runs that opted in via tenantVisibleRun — per job and subject every queued or running run plus the latest completed or failed one, active runs first, then newest first; each row carries only status and queuedAt/startedAt/finishedAt, never payloads, error text or logs; the optional subject filter matches exactly: all declared subjectFields with the same JSON types as the job payload; use it to show whether work for a subject is waiting, running or done, and to tell a failed run from a finished one.",
39
- schema: z.object({
40
- jobName: z.string().optional(),
41
- subject: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
42
- limit: z.number().int().min(1).max(200).optional(),
43
- }),
44
- // Every membership rank: a run state carries a job name, a status and
45
- // times, nothing a team member of the tenant may not see.
46
- access: { roles: access.roles("User", "Editor", ...access.admin) },
47
- handler: async (query, ctx) => {
48
- if (!ctx.systemDb) {
49
- throw new InternalError({
50
- message:
51
- "jobs:query:tenant-runs requires ctx.systemDb (feature must declare r.systemScope())",
35
+ export function createTenantRunsQuery(readRoles: readonly string[]) {
36
+ return defineQueryHandler({
37
+ name: "tenant-runs",
38
+ description:
39
+ "Lists the calling tenant's own job runs that opted in via tenantVisibleRun — per job and subject every queued or running run plus the latest completed or failed one, active runs first, then newest first; each row carries only status and queuedAt/startedAt/finishedAt, never payloads, error text or logs; the optional subject filter matches exactly: all declared subjectFields with the same JSON types as the job payload; use it to show whether work for a subject is waiting, running or done, and to tell a failed run from a finished one.",
40
+ schema: z.object({
41
+ jobName: z.string().optional(),
42
+ subject: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
43
+ limit: z.number().int().min(1).max(200).optional(),
44
+ }),
45
+ // Every membership rank: a run state carries a job name, a status and
46
+ // times, nothing a team member of the tenant may not see.
47
+ access: {
48
+ roles: [...new Set([...access.roles("User", "Editor", ...access.admin), ...readRoles])],
49
+ },
50
+ handler: async (query, ctx) => {
51
+ if (!ctx.systemDb) {
52
+ throw new InternalError({
53
+ message:
54
+ "jobs:query:tenant-runs requires ctx.systemDb (feature must declare r.systemScope())",
55
+ });
56
+ }
57
+ // assertTenantMatch returns the same unfiltered system-mode db — the
58
+ // explicit tenantId below is the filter, assertRowsTenant the second net.
59
+ const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
60
+ const where: Record<string, unknown> = { tenantId: [query.user.tenantId] };
61
+ if (query.payload.jobName) where["jobName"] = query.payload.jobName;
62
+ if (query.payload.subject) where["subject"] = serializeJobSubject(query.payload.subject);
63
+ const limit = query.payload.limit ?? DEFAULT_LIMIT;
64
+ const newestFirst = { col: "updatedAt", direction: "desc" } as const;
65
+ const activeRows = await selectMany<TenantJobRunRow>(
66
+ db,
67
+ tenantJobRunsTable,
68
+ { ...where, status: { in: ["queued", "running"] } },
69
+ { orderBy: newestFirst, limit },
70
+ );
71
+ const finishedRows = await selectMany<TenantJobRunRow>(
72
+ db,
73
+ tenantJobRunsTable,
74
+ { ...where, status: { in: ["completed", "failed"] } },
75
+ { orderBy: newestFirst, limit },
76
+ );
77
+ const seenFinishedKeys = new Set<string>();
78
+ const finishedNewestPerKey = finishedRows.filter((row) => {
79
+ const key = JSON.stringify([row.jobName, row.subject]);
80
+ if (seenFinishedKeys.has(key)) return false;
81
+ seenFinishedKeys.add(key);
82
+ return true;
52
83
  });
53
- }
54
- // assertTenantMatch returns the same unfiltered system-mode db — the
55
- // explicit tenantId below is the filter, assertRowsTenant the second net.
56
- const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
57
- const where: Record<string, unknown> = { tenantId: [query.user.tenantId] };
58
- if (query.payload.jobName) where["jobName"] = query.payload.jobName;
59
- if (query.payload.subject) where["subject"] = serializeJobSubject(query.payload.subject);
60
- const limit = query.payload.limit ?? DEFAULT_LIMIT;
61
- const newestFirst = { col: "updatedAt", direction: "desc" } as const;
62
- const activeRows = await selectMany<TenantJobRunRow>(
63
- db,
64
- tenantJobRunsTable,
65
- { ...where, status: { in: ["queued", "running"] } },
66
- { orderBy: newestFirst, limit },
67
- );
68
- const finishedRows = await selectMany<TenantJobRunRow>(
69
- db,
70
- tenantJobRunsTable,
71
- { ...where, status: { in: ["completed", "failed"] } },
72
- { orderBy: newestFirst, limit },
73
- );
74
- const seenFinishedKeys = new Set<string>();
75
- const finishedNewestPerKey = finishedRows.filter((row) => {
76
- const key = JSON.stringify([row.jobName, row.subject]);
77
- if (seenFinishedKeys.has(key)) return false;
78
- seenFinishedKeys.add(key);
79
- return true;
80
- });
81
- const activeFirst = [...activeRows, ...finishedNewestPerKey];
82
- return {
83
- rows: ctx.systemDb.assertRowsTenant(activeFirst.slice(0, limit), "tenantId").map((row) => ({
84
- jobName: row.jobName,
85
- subject: parseSubject(row.subject),
86
- status: row.status,
87
- queuedAt: row.queuedAt,
88
- startedAt: row.startedAt,
89
- finishedAt: row.finishedAt,
90
- })),
91
- nextCursor: null,
92
- };
93
- },
94
- });
84
+ const activeFirst = [...activeRows, ...finishedNewestPerKey];
85
+ return {
86
+ rows: ctx.systemDb.assertRowsTenant(activeFirst.slice(0, limit), "tenantId").map((row) => ({
87
+ jobName: row.jobName,
88
+ subject: parseSubject(row.subject),
89
+ status: row.status,
90
+ queuedAt: row.queuedAt,
91
+ startedAt: row.startedAt,
92
+ finishedAt: row.finishedAt,
93
+ })),
94
+ nextCursor: null,
95
+ };
96
+ },
97
+ });
98
+ }
@@ -262,15 +262,16 @@ async function recordTenantRunQueued(
262
262
  const now = Temporal.Now.instant();
263
263
  await transaction(db, async (tx) => {
264
264
  // A finished row under the same id is left over from before a Redis reset
265
- // (ids restart at 1), not this job. Only rows that went through this hook
266
- // (queuedAt set) qualify: a fast worker can finish before the hook runs,
267
- // and that row is this job's own and must stay.
265
+ // (ids restart at 1), not this job. Only rows queued before this BullMQ job
266
+ // was created qualify: a fast worker can finish before the hook runs (no
267
+ // queuedAt), and a deduplicated add of an already finished job re-announces
268
+ // it with a creation time older than its own row. Both rows must stay.
268
269
  await deleteMany(tx, tenantJobRunsTable, {
269
270
  tenantId: key.tenantId,
270
271
  jobName,
271
272
  bullJobId,
272
273
  status: { in: ["completed", "failed"] },
273
- queuedAt: { gt: Temporal.Instant.fromEpochMilliseconds(0) },
274
+ queuedAt: { lt: Temporal.Instant.fromEpochMilliseconds(meta.jobCreatedAtMs) },
274
275
  });
275
276
  // DO NOTHING: a fast worker can report the start before this enqueue hook
276
277
  // returns, and a repeated jobId (debounce, fan-out dedup) is the same job.
@@ -1,7 +1,7 @@
1
1
  // Client-safe ledger surface: QN constants, pure recurring helpers
2
2
  // (projection + Soll/Ist merge), and Zod payload schemas for client-side
3
3
  // pre-validation before dispatch. The full `../ledger` entry also re-exports
4
- // the feature/handlers/executor, which pull postgres-js; importing it in a
4
+ // the feature/handlers/executor, which pull the DB driver; importing it in a
5
5
  // browser bundle fails on Node builtins. Client screens import dispatch QNs and
6
6
  // pure forecast/merge helpers from this module, then dispatch via the renderer's
7
7
  // useDispatcher.
@@ -35,7 +35,7 @@ runProdApp({
35
35
  // decoupling — legal-pages imports no code from template-resolver,
36
36
  // only uses the API via ctx)
37
37
  anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
38
- extraContext: ({ db }) => ({
38
+ extraContext: ({ db, registry }) => ({
39
39
  templateResolver: createTemplateResolverApi(db, registry),
40
40
  }),
41
41
  });
@@ -42,12 +42,15 @@ async function loadBlocksFromJson(path: string): Promise<readonly LegalContentBl
42
42
 
43
43
  // Boot seed for runProdApp's `seeds: [...]`. The JSON template is authoritative
44
44
  // (ifExists "update"), so legally required text changes reach rows seeded once.
45
- export function legalContentSeed(source: LegalContentSeedSource): LegalContentSeedFn {
45
+ export function legalContentSeed(
46
+ source: LegalContentSeedSource,
47
+ options?: { readonly folder?: string },
48
+ ): LegalContentSeedFn {
46
49
  return async ({ db }) => {
47
50
  const blocks =
48
51
  "blocks" in source
49
52
  ? legalContentBlocksSchema.parse(source.blocks)
50
53
  : await loadBlocksFromJson(source.path);
51
- await seedLegalContentFromJson(db, blocks);
54
+ await seedLegalContentFromJson(db, blocks, options);
52
55
  };
53
56
  }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.388.0",
4
+ "type": "fix",
5
+ "title": "Tenant mail transport falls back to the app transport without tenant config",
6
+ "detail": "tenantMailTransport returns null when the channel context carries no tenant config, instead of throwing on every send, so the channel uses the app transport as documented."
7
+ },
2
8
  {
3
9
  "version": "0.378.0",
4
10
  "type": "improvement",
@@ -75,9 +75,9 @@ export const MAIL_TRANSPORT_EXTENSION = "mailTransport" as const;
75
75
  * config — tenant config reads (the plugin's host/port/from/...)
76
76
  * registry — extension lookup in the factory (not plugin-internal)
77
77
  * secrets — tenant secret reads (smtp.password)
78
- * auditUserId — audit identity for secret reads. Handler path: the
79
- * dispatcher sets the caller's user id; worker path: the
80
- * r.job wrap sets a system identity.
78
+ * auditUserId — audit identity for secret reads. Handler path: unset,
79
+ * requireSecretsContext falls back to ctx.user.id; worker path:
80
+ * the r.job wrap sets a system identity.
81
81
  */
82
82
  export type MailTransportContext = {
83
83
  readonly config?: ConfigAccessor;
@@ -276,6 +276,8 @@ export type TenantMailTransportScope = {
276
276
  export function tenantMailTransport(
277
277
  scope: TenantMailTransportScope,
278
278
  ): Promise<EmailTransport | null> {
279
+ // Without a tenant config the provider cannot be read: use the app transport.
280
+ if (scope.config === undefined) return Promise.resolve(null);
279
281
  return createTransportForTenantIfConfigured(
280
282
  {
281
283
  config: scope.config,
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.389.0",
4
+ "type": "improvement",
5
+ "title": "SMTP authentication is optional",
6
+ "detail": "authUser is no longer a required key and the SMTP password secret no longer counts as missing in readiness. With an empty user the transport connects without auth, which suits internal relays; with a user set, a missing password still fails the send."
7
+ },
2
8
  {
3
9
  "version": "0.370.0",
4
10
  "type": "breaking",
@@ -6,6 +6,7 @@ const tenantAdminWrite = access.withSystem(access.roles("TenantAdmin"));
6
6
  const tenantAdminRead = access.roles("TenantAdmin", "SystemAdmin");
7
7
 
8
8
  // required: true ↔ the requireNonEmpty calls in buildSmtpTransport — keep in sync.
9
+ // authUser stays optional: empty means an unauthenticated relay.
9
10
  export const SMTP_CONFIG_KEYS = {
10
11
  host: createTenantConfig("text", {
11
12
  required: true,
@@ -41,7 +42,6 @@ export const SMTP_CONFIG_KEYS = {
41
42
  },
42
43
  }),
43
44
  authUser: createTenantConfig("text", {
44
- required: true,
45
45
  default: "",
46
46
  write: tenantAdminWrite,
47
47
  read: tenantAdminRead,
@@ -105,8 +105,7 @@ export function createMailTransportSmtpFeature() {
105
105
  return `${plaintext.slice(0, 3)}...${plaintext.slice(-2)}`;
106
106
  },
107
107
  scope: "tenant",
108
- // required: true ↔ the missing-secret throw in readPassword — keep in sync.
109
- required: true,
108
+ // Only needed once authUser is set (readPassword throws then), so readiness must not demand it.
110
109
  });
111
110
 
112
111
  const configKeys = r.config({ keys: SMTP_CONFIG_KEYS });
@@ -191,14 +190,13 @@ async function buildSmtpTransport(
191
190
  "from",
192
191
  SMTP_HINT,
193
192
  );
194
- const authUser = requireNonEmpty(
195
- await ctxConfig(sharedMailTransportSmtpFeature.exports.configKeys.authUser),
196
- FEATURE_NAME,
197
- "authUser",
198
- SMTP_HINT,
199
- );
200
-
201
- const password = await readPassword(ctx, tenantId);
193
+ const authUser = (
194
+ await ctxConfig(sharedMailTransportSmtpFeature.exports.configKeys.authUser)
195
+ )?.trim();
196
+ const auth =
197
+ authUser === undefined || authUser === ""
198
+ ? undefined
199
+ : { user: authUser, pass: await readPassword(ctx, tenantId) };
202
200
 
203
201
  let target: MailConnectTarget;
204
202
  try {
@@ -227,7 +225,7 @@ async function buildSmtpTransport(
227
225
  port,
228
226
  secure,
229
227
  from,
230
- auth: { user: authUser, pass: password },
228
+ ...(auth && { auth }),
231
229
  ...(target.servername && { servername: target.servername }),
232
230
  });
233
231
  }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.389.0",
4
+ "type": "improvement",
5
+ "title": "MANAGED_PAGES_FEATURE names the feature",
6
+ "detail": "The barrel exports the MANAGED_PAGES_FEATURE constant so other code can refer to the feature name without a string literal."
7
+ },
2
8
  {
3
9
  "version": "0.363.0",
4
10
  "type": "improvement",
@@ -0,0 +1 @@
1
+ export const MANAGED_PAGES_FEATURE = "managed-pages" as const;
@@ -10,6 +10,7 @@ import {
10
10
  } from "../page-render/index.ts";
11
11
  import { rateLimitedTextResponse, type SystemQueryFn } from "../shared/index.ts";
12
12
  import { BRANDING_KEYS, BRANDING_QUERY_QN, CUSTOM_CSS_KEY, coerceBranding } from "./branding.ts";
13
+ import { MANAGED_PAGES_FEATURE } from "./constants.ts";
13
14
  import { createBrandingQuery } from "./handlers/branding.query.ts";
14
15
  import { bySlugQuery } from "./handlers/by-slug.query.ts";
15
16
  import { byTenantPublishedQuery } from "./handlers/by-tenant-published.query.ts";
@@ -153,7 +154,8 @@ export function createManagedPagesFeature(opts: ManagedPagesOptions): FeatureDef
153
154
  const defaultLang = opts.defaultLang ?? "en";
154
155
  const allowCustomCss = opts.allowCustomCss ?? false;
155
156
 
156
- return defineFeature("managed-pages", (r) => {
157
+ return defineFeature(MANAGED_PAGES_FEATURE, (r) => {
158
+ r.adoptsBundledLists();
157
159
  r.describe(
158
160
  "Tenant-editable, server-rendered public pages with per-tenant branding. Stores one Markdown `page` per `(tenantId, slug, lang)` in the `read_pages` entity table with a `published` gate plus `description`/`ogImage` SEO meta. Registers an anonymous `GET {basePath}/:slug` route that resolves the tenant from the request Host via the app-supplied `resolveApexTenant`, serves only published pages (drafts → 404), renders Markdown through the hardened `page-render` core, and isolates per-tenant content with `Vary: Host`. Ships TenantAdmin/SystemAdmin admin screens (`entityList` + `entityEdit`) backed by convention CRUD handlers (`managed-pages:write:page:{create,update,delete}`, `managed-pages:query:page:{list,detail}`); the app wires nav/workspace onto `managed-pages:screen:page-list`. Also exposes `managed-pages:query:by-tenant-published` (anonymous, SQL-filtered on `published`) for sitemap/discovery consumers such as the `seo` feature. Branding (via `config`, scope tenant): `branding-{title,description,site-url,accent-color,logo-url,layout-preset,custom-css}` keys with write-time validation (hex color, https URLs), a `configEdit` self-service screen (`managed-pages:screen:branding-settings`), and a `managed-pages:query:branding` read that the render path applies as scoped `:root` CSS vars + a logo/title header. Also exposes `managed-pages:write:set` (idempotent slug-keyed upsert, SystemAdmin cross-tenant via `tenantIdOverride`) as a provisioning API. Requires `config` + `anonymousAccess` wired at app bootstrap.",
159
161
  );
@@ -21,6 +21,7 @@ export {
21
21
  // (`config:write:set`) — single source for the `managed-pages:config:branding-*`
22
22
  // strings, so apps + the per-tenant migration never hardcode them.
23
23
  export { BRANDING_QN, MANAGED_PAGES_CSS_FEATURE } from "./branding.ts";
24
+ export { MANAGED_PAGES_FEATURE } from "./constants.ts";
24
25
  export { createManagedPagesCssFeature } from "./css-gate.ts";
25
26
  export {
26
27
  createManagedPagesFeature,
@@ -69,7 +69,7 @@ export function createAddNoteHandler(
69
69
  let authorName: string | null = null;
70
70
  try {
71
71
  // read_users is tenant-agnostic → ctx.db.unsafeRaw, not the tenant-scoped ctx.db.
72
- // postgres-js poisons the whole tx after any error inside it, even one that's
72
+ // Postgres aborts the whole tx after any error inside it, even one that's
73
73
  // caught — so the lookup runs in a savepoint where available, otherwise directly
74
74
  // (pool statements are their own units).
75
75
  authorName = await runInSavepointIfSupported(ctx.db.unsafeRaw(), async (sp) => {
@@ -36,7 +36,7 @@ const PAT_REVOKE_TRIGGERING_FIELDS = ["passwordHash"] as const;
36
36
 
37
37
  export type BindPatAutoRevokeOnPasswordChange = (
38
38
  revoker: (userId: string) => Promise<number>,
39
- ) => void;
39
+ ) => () => void;
40
40
 
41
41
  // Reads the late-bind setter off a mounted personal-access-tokens feature's
42
42
  // exports — run{Prod,Dev}App call this once a concrete db is available,
@@ -101,6 +101,7 @@ export function createPersonalAccessTokensFeature(
101
101
  ): FeatureDefinition {
102
102
  const { scopes } = options;
103
103
  return defineFeature(PAT_FEATURE, (r) => {
104
+ r.adoptsBundledLists();
104
105
  r.describe(
105
106
  "Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.",
106
107
  );
@@ -169,6 +170,9 @@ export function createPersonalAccessTokensFeature(
169
170
  });
170
171
  const bindAutoRevokeOnPasswordChange: BindPatAutoRevokeOnPasswordChange = (revoker) => {
171
172
  boundAutoRevoke = revoker;
173
+ return () => {
174
+ if (boundAutoRevoke === revoker) boundAutoRevoke = undefined;
175
+ };
172
176
  };
173
177
 
174
178
  const handlers = {
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.388.0",
4
+ "type": "fix",
5
+ "title": "renderTablePdf validates the document for direct callers",
6
+ "detail": "renderTablePdf parses the document against pdfTableDocumentSchema and throws RendererError invalid_payload above the row, column and cell caps. Title, subtitle, meta entries, headers, empty text, footer text and page label now have length caps and meta at most 500 entries."
7
+ },
2
8
  {
3
9
  "version": "0.364.0",
4
10
  "type": "fix",
@@ -7,10 +7,13 @@ import type { PdfPageFormat } from "../template-resolver/index.ts";
7
7
  const PDF_TABLE_MAX_ROWS = 10_000;
8
8
  const PDF_TABLE_MAX_COLUMNS = 30;
9
9
  const PDF_TABLE_MAX_CELL_LENGTH = 5_000;
10
+ const PDF_TABLE_MAX_META_ENTRIES = 500;
11
+ const PDF_TABLE_MAX_TEXT_LENGTH = 20_000;
12
+ const PDF_TABLE_MAX_PAGE_LABEL_LENGTH = 100;
10
13
 
11
14
  const pdfTableColumnSchema = z
12
15
  .object({
13
- header: z.string(),
16
+ header: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
14
17
  weight: z.number().positive().optional(),
15
18
  align: z.enum(["left", "right"]).optional(),
16
19
  })
@@ -18,10 +21,18 @@ const pdfTableColumnSchema = z
18
21
 
19
22
  export const pdfTableDocumentSchema = z
20
23
  .object({
21
- title: z.string(),
22
- subtitle: z.string().optional(),
24
+ title: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
25
+ subtitle: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH).optional(),
23
26
  meta: z
24
- .array(z.object({ label: z.string(), value: z.string() }).readonly())
27
+ .array(
28
+ z
29
+ .object({
30
+ label: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
31
+ value: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
32
+ })
33
+ .readonly(),
34
+ )
35
+ .max(PDF_TABLE_MAX_META_ENTRIES)
25
36
  .readonly()
26
37
  .optional(),
27
38
  columns: z.array(pdfTableColumnSchema).min(1).max(PDF_TABLE_MAX_COLUMNS).readonly(),
@@ -29,10 +40,10 @@ export const pdfTableDocumentSchema = z
29
40
  .array(z.array(z.string().max(PDF_TABLE_MAX_CELL_LENGTH)).readonly())
30
41
  .max(PDF_TABLE_MAX_ROWS)
31
42
  .readonly(),
32
- emptyText: z.string().optional(),
33
- footerText: z.string().optional(),
43
+ emptyText: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH).optional(),
44
+ footerText: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH).optional(),
34
45
  // Template with `{page}` and `{pages}`; default `"{page} / {pages}"`.
35
- pageLabel: z.string().optional(),
46
+ pageLabel: z.string().max(PDF_TABLE_MAX_PAGE_LABEL_LENGTH).optional(),
36
47
  })
37
48
  .readonly();
38
49
 
@@ -235,12 +246,18 @@ function resolveTableLayout(
235
246
  }
236
247
 
237
248
  export async function renderTablePdf(
238
- doc: PdfTableDocument,
249
+ untrustedDoc: PdfTableDocument,
239
250
  options?: PdfTableOptions,
240
251
  ): Promise<PdfTableResult> {
241
- if (doc.columns.length === 0) {
242
- throw new RendererError("table needs at least one column", "invalid_payload");
252
+ // Direct callers bypass renderTablePdfRequest, so the size caps are enforced here too.
253
+ const parsed = pdfTableDocumentSchema.safeParse(untrustedDoc);
254
+ if (!parsed.success) {
255
+ throw new RendererError(
256
+ `table document is invalid: ${parsed.error.message}`,
257
+ "invalid_payload",
258
+ );
243
259
  }
260
+ const doc = parsed.data;
244
261
  assertRowShape(doc);
245
262
 
246
263
  const pdf = await PDFDocument.create({ updateMetadata: false });
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.389.0",
4
+ "type": "fix",
5
+ "title": "Feature-prefixed undeclared secret keys are not deletable",
6
+ "detail": "The delete handler still accepts legacy undeclared keys, but a key starting with \"<feature>:\" of a mounted feature now fails as unknown key. Feature-owned credentials keep their writeRoles protection."
7
+ },
2
8
  {
3
9
  "version": "0.370.0",
4
10
  "type": "improvement",
@@ -28,6 +28,11 @@ function unknownKeyFailure(key: string): SecretKeyWriteCheck {
28
28
  };
29
29
  }
30
30
 
31
+ function isUnderMountedFeaturePrefix(registry: Registry, key: string): boolean {
32
+ const separator = key.indexOf(":");
33
+ return separator > 0 && registry.getFeature(key.slice(0, separator)) !== undefined;
34
+ }
35
+
31
36
  function namespaceAcceptsKey(namespace: SecretNamespaceDefinition, key: string): boolean {
32
37
  const suffix = key.slice(namespace.qualifiedPrefix.length);
33
38
  if (suffix.length === 0) return false;
@@ -38,7 +43,9 @@ function namespaceAcceptsKey(namespace: SecretNamespaceDefinition, key: string):
38
43
  // are writable through the API, so a tenant admin cannot plant arbitrary rows
39
44
  // next to the feature-owned ones. writeRoles narrows the handler access
40
45
  // further for a key or a whole namespace. Delete passes allowUndeclared: it
41
- // creates nothing, and legacy rows under since-undeclared keys must stay removable.
46
+ // creates nothing, and legacy rows under since-undeclared keys must stay removable,
47
+ // unless the key sits under a mounted feature's prefix: those rows belong to the
48
+ // feature and must not be removable by a tenant admin outside its writeRoles.
42
49
  export function checkSecretKeyWrite(
43
50
  registry: Registry,
44
51
  userRoles: readonly string[],
@@ -50,7 +57,7 @@ export function checkSecretKeyWrite(
50
57
  if (
51
58
  !keyDef &&
52
59
  !(namespace && namespaceAcceptsKey(namespace, key)) &&
53
- options.allowUndeclared !== true
60
+ (options.allowUndeclared !== true || isUnderMountedFeaturePrefix(registry, key))
54
61
  ) {
55
62
  return unknownKeyFailure(key);
56
63
  }
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.389.0",
4
+ "type": "breaking",
5
+ "title": "isPrincipalBlocked is no longer exported from sessions",
6
+ "detail": "The sessions export dropped its re-export of isPrincipalBlocked, which ignored isDeleted. isPrincipalRowBlocked from the user export covers status and isDeleted and is what the session callbacks use.",
7
+ "migration": "import isPrincipalRowBlocked (for a user row) or isPrincipalBlocked (for a bare status) from \"@cosmicdrift/kumiko-bundled-features/user\" instead of the sessions export"
8
+ },
2
9
  {
3
10
  "version": "0.384.0",
4
11
  "type": "improvement",
@@ -59,8 +59,10 @@ export type SessionsFeatureOptions = {
59
59
  readonly adminAccess?: "admin" | "systemAdmin";
60
60
  };
61
61
 
62
- export type BindAutoRevokeOnPasswordChange = (revoker: SessionMassRevoker) => void;
63
- export type BindRevokeAllOtherSessions = (revoker: SessionAllOthersRevoker) => void;
62
+ // The returned unbind only clears the binding while it is still this revoker, so a stack that
63
+ // stops later cannot unbind a newer stack sharing the same feature instance.
64
+ export type BindAutoRevokeOnPasswordChange = (revoker: SessionMassRevoker) => () => void;
65
+ export type BindRevokeAllOtherSessions = (revoker: SessionAllOthersRevoker) => () => void;
64
66
 
65
67
  // Reads the late-bind setter off a mounted sessions feature's exports.
66
68
  // run{Prod,Dev}App call it once the DB connection is concrete — the feature
@@ -100,6 +102,7 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
100
102
  "sessions",
101
103
  (r) => {
102
104
  const adminRoles = access[options?.adminAccess ?? "admin"];
105
+ r.adoptsBundledLists();
103
106
  r.describe(
104
107
  "Tracks signed-in clients in the `store_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`. Session creation and revocation on the hot auth path are handled by `createSessionCallbacks()`, wired into `buildServer({ auth: { ... } })` outside the dispatcher; the same callbacks are also registered as an auth-foundation sessionStore provider, resolvable generically via `resolveSessionStore()`. The feature also ships a manual-trigger cleanup job for pruning expired rows and an optional `autoRevokeOnPasswordChange` hook that mass-revokes all sessions for a user whenever their `passwordHash` changes.",
105
108
  );
@@ -236,6 +239,9 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
236
239
 
237
240
  const bindAutoRevokeOnPasswordChange: BindAutoRevokeOnPasswordChange = (revoker) => {
238
241
  boundAutoRevoke = revoker;
242
+ return () => {
243
+ if (boundAutoRevoke === revoker) boundAutoRevoke = undefined;
244
+ };
239
245
  };
240
246
 
241
247
  r.translations({ keys: SESSIONS_I18N });
@@ -15,7 +15,7 @@ export type {
15
15
  SessionCallbacksOptions,
16
16
  SessionMassRevoker,
17
17
  } from "./session-callbacks.ts";
18
- export { createSessionCallbacks, isPrincipalBlocked } from "./session-callbacks.ts";
18
+ export { createSessionCallbacks } from "./session-callbacks.ts";
19
19
  export type { SessionRevokedPayload } from "./session-revoked-event.ts";
20
20
  export {
21
21
  SESSION_REVOKED_AGGREGATE_TYPE,
@@ -18,12 +18,7 @@ import { generateId, parseRoles } from "@cosmicdrift/kumiko-framework/utils";
18
18
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
19
19
  import { encryptForDirectWrite } from "../shared/index.ts";
20
20
  import { tenantMembershipsTable } from "../tenant/index.ts";
21
- import {
22
- isPrincipalBlocked,
23
- isPrincipalRowBlocked,
24
- type UserStatus,
25
- userTable,
26
- } from "../user/index.ts";
21
+ import { isPrincipalRowBlocked, type UserStatus, userTable } from "../user/index.ts";
27
22
  import {
28
23
  DEFAULT_SESSION_EXPIRY_MS,
29
24
  LAST_SEEN_REFRESH_MS,
@@ -36,9 +31,6 @@ import {
36
31
  sessionRevokedSchema,
37
32
  } from "./session-revoked-event.ts";
38
33
 
39
- // Re-exported so existing `../sessions` importers keep working unchanged.
40
- export { isPrincipalBlocked };
41
-
42
34
  type SettledLookup<T> = { readonly ok: true; readonly value: T } | { readonly ok: false };
43
35
 
44
36
  function settleLookup<T>(lookup: Promise<T>): Promise<SettledLookup<T>> {
@@ -2,6 +2,7 @@ import { requestContext } from "@cosmicdrift/kumiko-framework/api";
2
2
  import {
3
3
  configuredPiiSubjectKms,
4
4
  decryptPiiFieldValues,
5
+ PII_ERASED_SENTINEL,
5
6
  } from "@cosmicdrift/kumiko-framework/crypto";
6
7
 
7
8
  // Raw reads (fetchOne/selectMany) return the stored column value — with an
@@ -24,3 +25,12 @@ export async function decryptStoredPii(
24
25
  const decrypted = out[field];
25
26
  return typeof decrypted === "string" ? decrypted : value;
26
27
  }
28
+
29
+ // Null for an erased subject: the sentinel must never be mailed or shown as an address.
30
+ export async function decryptStoredUserEmail(
31
+ stored: string,
32
+ fallbackRequestId: string,
33
+ ): Promise<string | null> {
34
+ const email = await decryptStoredPii(stored, "email", fallbackRequestId);
35
+ return email === PII_ERASED_SENTINEL ? null : email;
36
+ }
@@ -15,7 +15,7 @@ export {
15
15
  classifyStoredEnvelope,
16
16
  type StoredEnvelopeClassification,
17
17
  } from "./classify-stored-envelope.ts";
18
- export { decryptStoredPii } from "./decrypt-stored-pii.ts";
18
+ export { decryptStoredPii, decryptStoredUserEmail } from "./decrypt-stored-pii.ts";
19
19
  export { encryptForDirectWrite } from "./encrypt-for-direct-write.ts";
20
20
  export { entitiesOf } from "./entities-of.ts";
21
21
  export { isWithinGracePeriod } from "./grace-period.ts";