@cosmicdrift/kumiko-bundled-features 0.333.0 → 0.335.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 (277) hide show
  1. package/dist/admin-shell/overview-panels.js +2 -2
  2. package/dist/agent-tools/agent-doc-lint.d.ts +1 -0
  3. package/dist/agent-tools/agent-doc-lint.js +33 -3
  4. package/dist/agent-tools/agent-manifest.js +47 -6
  5. package/dist/agent-tools/filter-ops.d.ts +2 -0
  6. package/dist/agent-tools/filter-ops.js +4 -0
  7. package/dist/agent-tools/tool-catalog.js +1 -4
  8. package/dist/agent-tools/tool-dispatch.js +1 -4
  9. package/dist/audit/constants.d.ts +1 -1
  10. package/dist/audit/constants.js +1 -1
  11. package/dist/audit/handlers/list.query.js +4 -3
  12. package/dist/auth-email-password/handlers/change-password.write.js +2 -0
  13. package/dist/auth-email-password/handlers/invite-create.write.js +12 -2
  14. package/dist/auth-email-password/handlers/login.write.js +2 -0
  15. package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
  16. package/dist/auth-email-password/i18n.js +2 -0
  17. package/dist/auth-email-password/web/auth-client.d.ts +1 -0
  18. package/dist/auth-email-password/web/auth-client.js +1 -1
  19. package/dist/auth-email-password/web/auth-gate.js +6 -8
  20. package/dist/auth-email-password/web/index.d.ts +1 -1
  21. package/dist/auth-email-password/web/index.js +1 -1
  22. package/dist/auth-email-password/web/invite-accept-screen.js +16 -12
  23. package/dist/auth-email-password/web/login-screen.js +1 -1
  24. package/dist/auth-email-password/web/session-bootstrap-error.d.ts +5 -1
  25. package/dist/auth-email-password/web/session-bootstrap-error.js +2 -2
  26. package/dist/auth-email-password/web/session.d.ts +4 -0
  27. package/dist/auth-email-password/web/session.js +15 -0
  28. package/dist/auth-mfa/feature.js +3 -2
  29. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +2 -2
  30. package/dist/auth-mfa/handlers/verify.write.js +2 -0
  31. package/dist/auth-mfa/hmac-token-codec.js +3 -0
  32. package/dist/auth-mfa/mfa-challenge-token.js +4 -0
  33. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -0
  34. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
  35. package/dist/auth-mfa/web/mfa-client.d.ts +1 -1
  36. package/dist/auth-mfa/web/mfa-client.js +4 -10
  37. package/dist/billing-foundation/billing-info-query.js +1 -1
  38. package/dist/billing-foundation/checkout-core.d.ts +5 -5
  39. package/dist/billing-foundation/checkout-core.js +10 -6
  40. package/dist/billing-foundation/entities.d.ts +8 -0
  41. package/dist/billing-foundation/handlers/switch-plan.write.js +5 -8
  42. package/dist/billing-foundation/projection.d.ts +8 -0
  43. package/dist/cap-counter/book-cap-usage.js +15 -5
  44. package/dist/cap-counter/enforce-cap.js +9 -3
  45. package/dist/cap-counter/entity.d.ts +1 -0
  46. package/dist/cap-counter/feature.js +3 -2
  47. package/dist/cap-counter/i18n.d.ts +1 -0
  48. package/dist/cap-counter/i18n.js +6 -5
  49. package/dist/cap-overview/handlers/caps-usage.query.js +9 -16
  50. package/dist/cap-overview/handlers/tenant-caps-list.query.js +41 -15
  51. package/dist/cap-overview/i18n.js +5 -0
  52. package/dist/cap-overview/web/cap-cards-panel.js +4 -1
  53. package/dist/cap-overview/web/cap-usage-bar.js +1 -1
  54. package/dist/channel-email/email-channel.js +12 -1
  55. package/dist/channel-in-app/tables.js +2 -6
  56. package/dist/compliance-profiles/handlers/sub-processors.query.js +1 -1
  57. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
  58. package/dist/config/db/queries/resolver.js +3 -1
  59. package/dist/config/handlers/set.write.js +8 -4
  60. package/dist/config/table.d.ts +6 -0
  61. package/dist/config/web/i18n.js +13 -4
  62. package/dist/config/write-helpers.d.ts +1 -1
  63. package/dist/config/write-helpers.js +11 -5
  64. package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -8
  65. package/dist/custom-fields/constants.js +1 -4
  66. package/dist/custom-fields/entity.d.ts +4 -0
  67. package/dist/data-retention/resolve-for-tenant.d.ts +2 -3
  68. package/dist/data-retention/run-retention-cleanup.js +100 -44
  69. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
  70. package/dist/delivery/tables.d.ts +6 -0
  71. package/dist/delivery/types.d.ts +2 -1
  72. package/dist/delivery/types.js +7 -0
  73. package/dist/delivery/unsubscribe.js +5 -11
  74. package/dist/derivatives-sharp/render.js +27 -9
  75. package/dist/document-ingest-foundation/entity.d.ts +4 -0
  76. package/dist/document-ingest-foundation/feature.js +5 -5
  77. package/dist/document-ingest-foundation/providers.d.ts +1 -0
  78. package/dist/document-ingest-foundation/providers.js +6 -0
  79. package/dist/document-ingest-foundation/write-document-extract.d.ts +2 -2
  80. package/dist/document-ingest-foundation/write-document-extract.js +10 -0
  81. package/dist/feature-toggles/global-feature-state-table.js +2 -7
  82. package/dist/feature-toggles/toggle-sync-signal.d.ts +2 -1
  83. package/dist/feature-toggles/toggle-sync-signal.js +3 -2
  84. package/dist/file-provider-s3/feature.js +1 -1
  85. package/dist/files-tenant-data/index.js +3 -2
  86. package/dist/folders/entity.d.ts +8 -0
  87. package/dist/folders/handlers/clear-folder.write.js +4 -11
  88. package/dist/folders/handlers/set-folder.write.js +4 -10
  89. package/dist/form-draft/entity.d.ts +2 -0
  90. package/dist/form-draft/feature.js +3 -2
  91. package/dist/form-draft/handlers/discard.write.js +7 -8
  92. package/dist/form-draft/handlers/get.query.js +7 -1
  93. package/dist/foundation-shared/host-allowlist.d.ts +4 -0
  94. package/dist/foundation-shared/host-allowlist.js +14 -0
  95. package/dist/foundation-shared/index.d.ts +1 -0
  96. package/dist/foundation-shared/index.js +1 -0
  97. package/dist/foundation-shared/mail-host-policy.d.ts +0 -2
  98. package/dist/foundation-shared/mail-host-policy.js +3 -13
  99. package/dist/inbound-mail-foundation/connect-routes.js +7 -5
  100. package/dist/inbound-mail-foundation/entities.d.ts +25 -0
  101. package/dist/inbound-mail-foundation/projection.d.ts +20 -0
  102. package/dist/inbound-mail-foundation/watch-supervisor.js +47 -43
  103. package/dist/jobs/handlers/list.query.d.ts +7 -2
  104. package/dist/jobs/handlers/list.query.js +9 -2
  105. package/dist/jobs/handlers/retry.write.js +3 -2
  106. package/dist/jobs/handlers/tenant-failures.query.js +3 -9
  107. package/dist/jobs/job-run-logger.js +18 -2
  108. package/dist/jobs/job-run-table.d.ts +6 -0
  109. package/dist/jobs/tenant-job-failure-table.js +2 -6
  110. package/dist/ledger/entity.d.ts +16 -3
  111. package/dist/ledger/handlers/confirm-schedule-period.write.js +2 -2
  112. package/dist/ledger/handlers/reverse-transaction.write.js +2 -2
  113. package/dist/ledger/subject-reference.d.ts +4 -0
  114. package/dist/ledger/subject-reference.js +7 -0
  115. package/dist/legal-pages/feature.js +8 -1
  116. package/dist/managed-pages/handlers/branding.query.js +1 -1
  117. package/dist/managed-pages/handlers/by-slug.query.js +1 -1
  118. package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -1
  119. package/dist/managed-pages/table.d.ts +12 -0
  120. package/dist/metrics/compute.js +7 -1
  121. package/dist/metrics/window.js +5 -0
  122. package/dist/notes-history/entity.d.ts +12 -0
  123. package/dist/notes-history/handlers/add-note.write.js +4 -9
  124. package/dist/notes-history-user-data/hooks.js +8 -1
  125. package/dist/personal-access-tokens/constants.d.ts +1 -0
  126. package/dist/personal-access-tokens/constants.js +1 -0
  127. package/dist/personal-access-tokens/feature.js +2 -2
  128. package/dist/personal-access-tokens/handlers/create.write.js +2 -2
  129. package/dist/personal-access-tokens/handlers/revoke.write.js +2 -2
  130. package/dist/personal-access-tokens/i18n.js +12 -13
  131. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
  132. package/dist/personal-access-tokens/schema/api-token.js +2 -1
  133. package/dist/personal-access-tokens/screens.js +5 -1
  134. package/dist/rate-limiting/feature.js +2 -0
  135. package/dist/rate-limiting/handlers/bucket-access.js +15 -7
  136. package/dist/rate-limiting/i18n.d.ts +10 -0
  137. package/dist/rate-limiting/i18n.js +10 -0
  138. package/dist/secrets/feature.js +3 -2
  139. package/dist/secrets/table.d.ts +1 -0
  140. package/dist/seo/handlers/seo-config.query.js +1 -1
  141. package/dist/sessions/constants.d.ts +1 -0
  142. package/dist/sessions/constants.js +1 -0
  143. package/dist/sessions/feature.js +5 -3
  144. package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -0
  145. package/dist/sessions/handlers/revoke-all-for-user.write.js +12 -2
  146. package/dist/sessions/handlers/revoke.write.js +2 -2
  147. package/dist/sessions/schema/user-session.d.ts +4 -0
  148. package/dist/sessions/schema/user-session.js +2 -1
  149. package/dist/sessions/screens.js +5 -1
  150. package/dist/sessions/session-callbacks.js +2 -2
  151. package/dist/shared/index.d.ts +1 -1
  152. package/dist/shared/index.js +1 -1
  153. package/dist/shared/lockout-counter.js +1 -1
  154. package/dist/shared/parent-visibility.d.ts +5 -0
  155. package/dist/shared/parent-visibility.js +17 -1
  156. package/dist/shared/row-bound-grant.js +20 -9
  157. package/dist/shared/single-use-token-store.js +5 -13
  158. package/dist/step-dispatcher/webhook-runner.d.ts +0 -2
  159. package/dist/step-dispatcher/webhook-runner.js +18 -17
  160. package/dist/subscription-mollie/plugin-methods.d.ts +3 -2
  161. package/dist/subscription-mollie/plugin-methods.js +7 -1
  162. package/dist/subscription-mollie/verify-webhook.js +6 -0
  163. package/dist/subscription-stripe/feature.js +3 -3
  164. package/dist/subscription-stripe/plugin-methods.js +25 -6
  165. package/dist/subscription-stripe/verify-webhook.js +13 -6
  166. package/dist/tags/entity.d.ts +9 -0
  167. package/dist/tags/handlers/assign-tag.write.js +4 -10
  168. package/dist/tags/handlers/remove-tag.write.js +4 -11
  169. package/dist/template-resolver/api.d.ts +2 -1
  170. package/dist/template-resolver/api.js +1 -1
  171. package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
  172. package/dist/template-resolver/handlers/by-slug.query.js +2 -2
  173. package/dist/template-resolver/handlers/by-tenant.query.d.ts +3 -2
  174. package/dist/template-resolver/handlers/by-tenant.query.js +2 -2
  175. package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
  176. package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
  177. package/dist/template-resolver/handlers/collection-shared.d.ts +3 -2
  178. package/dist/template-resolver/handlers/collection-shared.js +1 -1
  179. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
  180. package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
  181. package/dist/template-resolver/handlers/list.query.d.ts +1 -1
  182. package/dist/template-resolver/handlers/list.query.js +1 -1
  183. package/dist/template-resolver/seeding.js +41 -39
  184. package/dist/template-resolver/table.d.ts +26 -13
  185. package/dist/template-resolver/testing.js +2 -2
  186. package/dist/template-resolver/user-content-table.d.ts +16 -9
  187. package/dist/template-resolver/web/client-plugin.d.ts +1 -1
  188. package/dist/template-resolver-user-data/hooks.js +1 -1
  189. package/dist/tenant/cancel-pending-invitation.d.ts +8 -0
  190. package/dist/tenant/cancel-pending-invitation.js +22 -0
  191. package/dist/tenant/feature.js +4 -2
  192. package/dist/tenant/handlers/cancel-invitation.write.js +6 -21
  193. package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
  194. package/dist/tenant/handlers/member-directory.query.js +17 -4
  195. package/dist/tenant/handlers/remove-member.write.js +9 -14
  196. package/dist/tenant/handlers/team-list.query.js +2 -2
  197. package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
  198. package/dist/tenant/handlers/tenant-directory.query.js +13 -3
  199. package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
  200. package/dist/tenant/handlers/update-member-roles.write.js +28 -12
  201. package/dist/tenant/i18n.js +4 -0
  202. package/dist/tenant/index.d.ts +1 -1
  203. package/dist/tenant/index.js +1 -1
  204. package/dist/tenant/invitation-table.d.ts +8 -2
  205. package/dist/tenant/last-tenant-admin.d.ts +1 -1
  206. package/dist/tenant/last-tenant-admin.js +5 -3
  207. package/dist/tenant/membership-table.d.ts +4 -0
  208. package/dist/tenant/schema/tenant.d.ts +8 -2
  209. package/dist/tenant/seeding.d.ts +4 -0
  210. package/dist/tenant/seeding.js +9 -2
  211. package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
  212. package/dist/tenant-lifecycle/boot-checks.js +27 -1
  213. package/dist/tenant-lifecycle/feature.js +2 -1
  214. package/dist/tenant-lifecycle/stages.js +2 -2
  215. package/dist/tenant-settings/feature.js +9 -6
  216. package/dist/tier-engine/entity.d.ts +2 -0
  217. package/dist/tier-engine/feature.js +3 -2
  218. package/dist/tier-engine/tier-resolver.d.ts +2 -0
  219. package/dist/user/command-schemas.d.ts +2 -2
  220. package/dist/user/handlers/create.write.d.ts +1 -1
  221. package/dist/user/handlers/create.write.js +8 -2
  222. package/dist/user/handlers/update.write.d.ts +1 -1
  223. package/dist/user/handlers/update.write.js +8 -2
  224. package/dist/user/schema/user.d.ts +14 -2
  225. package/dist/user-data-rights/db/queries/export-jobs.d.ts +2 -0
  226. package/dist/user-data-rights/feature.js +22 -9
  227. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +4 -7
  228. package/dist/user-data-rights/handlers/deletion-grace-period.d.ts +3 -2
  229. package/dist/user-data-rights/handlers/deletion-grace-period.js +30 -8
  230. package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
  231. package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
  232. package/dist/user-data-rights/handlers/request-deletion.write.js +3 -6
  233. package/dist/user-data-rights/lib/update-user-lifecycle.js +7 -14
  234. package/dist/user-data-rights/run-export-jobs.d.ts +1 -0
  235. package/dist/user-data-rights/run-export-jobs.js +74 -41
  236. package/dist/user-data-rights/run-forget-cleanup.js +2 -2
  237. package/dist/user-data-rights/run-user-export.js +8 -4
  238. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
  239. package/dist/user-data-rights/schema/download-token.d.ts +8 -0
  240. package/dist/user-data-rights/schema/export-job.d.ts +8 -2
  241. package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +4 -1
  242. package/dist/user-profile/feature.js +6 -1
  243. package/dist/user-profile/handlers/change-email.write.js +2 -0
  244. package/dist/workflow-runner/db/queries/due-runs.js +6 -9
  245. package/dist/workflow-runner/event-subscriber.js +29 -9
  246. package/dist/workflow-runner/handlers/resume-run.write.js +8 -5
  247. package/dist/workflow-runner/pending-projection.js +8 -1
  248. package/package.json +11 -9
  249. package/src/agent-tools/changes.json +12 -0
  250. package/src/auth-email-password/changes.json +18 -0
  251. package/src/auth-mfa/changes.json +6 -0
  252. package/src/billing-foundation/changes.json +7 -0
  253. package/src/cap-counter/changes.json +7 -0
  254. package/src/cap-overview/changes.json +12 -0
  255. package/src/channel-email/changes.json +6 -0
  256. package/src/config/changes.json +6 -0
  257. package/src/crypto-shredding/changes.json +6 -0
  258. package/src/data-retention/changes.json +12 -0
  259. package/src/delivery/changes.json +7 -0
  260. package/src/derivatives-sharp/changes.json +11 -0
  261. package/src/document-ingest-foundation/changes.json +12 -0
  262. package/src/inbound-mail-foundation/changes.json +12 -0
  263. package/src/jobs/changes.json +13 -0
  264. package/src/legal-pages/changes.json +8 -1
  265. package/src/managed-pages/changes.json +5 -0
  266. package/src/metrics/changes.json +6 -0
  267. package/src/rate-limiting/changes.json +5 -0
  268. package/src/secrets/changes.json +7 -0
  269. package/src/sessions/changes.json +5 -0
  270. package/src/shared/changes.json +6 -0
  271. package/src/step-dispatcher/changes.json +5 -0
  272. package/src/subscription-mollie/changes.json +7 -0
  273. package/src/subscription-stripe/changes.json +11 -0
  274. package/src/template-resolver/changes.json +7 -0
  275. package/src/tenant/changes.json +29 -0
  276. package/src/user-data-rights/changes.json +22 -0
  277. package/src/workflow-runner/changes.json +6 -0
@@ -11,9 +11,14 @@ export const CAP_OVERVIEW_I18N = {
11
11
  "cap-overview.cards.empty": { en: "No caps configured for this tenant." },
12
12
  "cap-overview.cards.loading": { en: "Loading usage…" },
13
13
  "cap-overview.notMeasured": { en: "Not measured yet" },
14
+ "cap-overview.unlimited": { en: "{used} · unlimited" },
14
15
  "cap-overview.errors.progressPrimitiveMissing": {
15
16
  en: "Usage bar unavailable — Progress primitive is not registered.",
16
17
  },
18
+ "cap-overview.errors.invalidCursor": { en: "The pagination cursor is invalid." },
19
+ "cap-overview.errors.filterFieldUnsupported": {
20
+ en: "Filtering by this field is not supported, or tier was filtered more than once.",
21
+ },
17
22
  "cap-overview.errors.sortFieldUnsupported": { en: "Sorting by this field is not supported." },
18
23
  "cap-overview.errors.tenantOverrideRequiresSystemAdmin": {
19
24
  en: "Only SystemAdmin may query another tenant's data.",
@@ -52,10 +52,13 @@ export function CapCardsPanel({ filterParams }) {
52
52
  // rule exists, since that file IS in the scanned surface.
53
53
  className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", "data-testid": "cap-cards-panel", children: [query.loading && rows.length === 0 && (_jsx("div", { className: "text-sm text-muted-foreground", children: t("cap-overview.cards.loading") })), rows.map((cap) => {
54
54
  const Icon = cap.icon !== undefined ? CAP_ICONS[cap.icon] : undefined;
55
+ // An unlimited, measured cap has neither a badge nor a bar; StatCard still
56
+ // wraps any defined child in a spacing div, so pass undefined then.
57
+ const body = !isMeasured(cap) || cap.limit !== null ? (_jsxs("div", { className: "flex flex-col gap-2", children: [cap.percent !== null && (_jsx("div", { className: "flex items-center justify-between gap-2", children: _jsx(StatusBadge, { tone: TONE_TO_STATUS[cap.tone], children: `${cap.percent}%` }) })), _jsx(CapUsageBar, { usage: cap, showLabel: false })] })) : undefined;
55
58
  return (_jsx(StatCard, { icon: Icon !== undefined ? _jsx(Icon, { className: "size-4" }) : undefined, label: t(cap.label), value: !isMeasured(cap)
56
59
  ? "—"
57
60
  : cap.limit === null
58
61
  ? String(cap.used)
59
- : `${cap.used} / ${cap.limit}`, ...(cap.accentColor !== undefined && { accentColor: cap.accentColor }), testId: "cap-card", children: _jsxs("div", { className: "flex flex-col gap-2", children: [cap.percent !== null && (_jsx("div", { className: "flex items-center justify-between gap-2", children: _jsx(StatusBadge, { tone: TONE_TO_STATUS[cap.tone], children: `${cap.percent}%` }) })), _jsx(CapUsageBar, { usage: cap, showLabel: false })] }) }, cap.id));
62
+ : `${cap.used} / ${cap.limit}`, ...(cap.accentColor !== undefined && { accentColor: cap.accentColor }), testId: "cap-card", children: body }, cap.id));
60
63
  })] }));
61
64
  }
@@ -10,7 +10,7 @@ export function CapUsageBar({ usage, showLabel = true, }) {
10
10
  }
11
11
  if (usage.limit === null) {
12
12
  // A progress bar with no denominator says nothing — show the raw count instead.
13
- return showLabel ? (_jsx(Text, { variant: "small", testId: "cap-usage-unlimited", children: String(usage.used) })) : null;
13
+ return showLabel ? (_jsx(Text, { variant: "small", testId: "cap-usage-unlimited", children: t("cap-overview.unlimited", { used: usage.used }) })) : null;
14
14
  }
15
15
  return (_jsxs("div", { className: "flex flex-col gap-1", children: [Progress ? (_jsx(Progress, { value: usage.fraction, tone: computeTone(usage.fraction), testId: "cap-usage-bar" })) : (_jsx(Banner, { variant: "error", testId: "cap-usage-bar-missing-primitive", children: t("cap-overview.errors.progressPrimitiveMissing") })), showLabel && _jsx(Text, { variant: "small", children: `${usage.used} / ${usage.limit}` })] }));
16
16
  }
@@ -15,6 +15,10 @@ function stringHeaders(raw) {
15
15
  }
16
16
  return Object.keys(out).length > 0 ? out : undefined;
17
17
  }
18
+ const UNSUBSCRIBE_HEADER_NAMES = new Set([
19
+ "list-unsubscribe",
20
+ "list-unsubscribe-post",
21
+ ]);
18
22
  // Only the framework's own unsubscribe route can honor a one-click POST.
19
23
  function listUnsubscribeHeaders(data) {
20
24
  const rawUrl = data?.["unsubscribeUrl"];
@@ -43,7 +47,14 @@ function emailEnvelopeFrom(data) {
43
47
  const replyTo = typeof data["replyTo"] === "string" ? data["replyTo"] : undefined;
44
48
  const autoHeaders = listUnsubscribeHeaders(data);
45
49
  const explicitHeaders = stringHeaders(data["headers"]);
46
- const headers = autoHeaders || explicitHeaders ? { ...autoHeaders, ...explicitHeaders } : undefined;
50
+ // Header names are case-insensitive, and the auto pair only works together:
51
+ // an explicit override of either half must drop both, or a mail client would
52
+ // one-click POST to a URL that cannot handle it.
53
+ const overridesUnsubscribe = explicitHeaders !== undefined &&
54
+ Object.keys(explicitHeaders).some((name) => UNSUBSCRIBE_HEADER_NAMES.has(name.toLowerCase()));
55
+ const headers = (!overridesUnsubscribe && autoHeaders) || explicitHeaders
56
+ ? { ...(overridesUnsubscribe ? undefined : autoHeaders), ...explicitHeaders }
57
+ : undefined;
47
58
  return { ...(from && { from }), ...(replyTo && { replyTo }), ...(headers && { headers }) };
48
59
  }
49
60
  export function createEmailChannel(options) {
@@ -1,4 +1,4 @@
1
- import { asEntityTableMeta } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { requireEntityTableMeta } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { boolean, instant, table as pgTable, serial, sql, text, uuid, } from "@cosmicdrift/kumiko-framework/db";
3
3
  export const inAppMessagesTable = pgTable("in_app_messages", {
4
4
  id: serial("id").primaryKey(),
@@ -14,8 +14,4 @@ export const inAppMessagesTable = pgTable("in_app_messages", {
14
14
  });
15
15
  // Derived from inAppMessagesTable (table() attaches the meta as a Symbol) so
16
16
  // the two can't drift, instead of hand-duplicating the column list.
17
- const derivedInAppMessagesTableMeta = asEntityTableMeta(inAppMessagesTable);
18
- if (!derivedInAppMessagesTableMeta) {
19
- throw new Error("inAppMessagesTable: table carries no EntityTableMeta — built via table()?");
20
- }
21
- export const inAppMessagesTableMeta = derivedInAppMessagesTableMeta;
17
+ export const inAppMessagesTableMeta = requireEntityTableMeta(inAppMessagesTable, "inAppMessagesTable");
@@ -19,7 +19,7 @@ export const subProcessorsQuery = defineQueryHandler({
19
19
  name: "sub-processors",
20
20
  schema: z.object({}),
21
21
  access: { roles: ["anonymous", "Member", "User", "TenantAdmin", "SystemAdmin"] },
22
- rateLimit: { per: "ip", limit: 30, windowSeconds: 60 },
22
+ rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
23
23
  description: "Returns the publicly readable Kumiko sub-processor list split into active and planned entries, for the GDPR Art. 28(2) disclosure a privacy policy or data-processing agreement links to.",
24
24
  handler: async () => {
25
25
  return {
@@ -17,7 +17,7 @@ export declare const tenantComplianceProfileEntity: import("@cosmicdrift/kumiko-
17
17
  readonly "swiss-dsg"?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
18
18
  } | undefined;
19
19
  readonly optionsQuery?: string;
20
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
20
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
21
21
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
22
22
  required: true;
23
23
  };
@@ -57,7 +57,7 @@ export declare const tenantComplianceProfileTable: import("@cosmicdrift/kumiko-f
57
57
  readonly "swiss-dsg"?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
58
58
  } | undefined;
59
59
  readonly optionsQuery?: string;
60
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
60
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
61
61
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
62
62
  required: true;
63
63
  };
@@ -1,6 +1,8 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { configValuesTable } from "../../table.js";
3
- // Method-form so a tenant-mode TenantDb stays filtered.
3
+ // Method-form so a tenant-mode TenantDb stays filtered. Two parallel selects
4
+ // (disjoint via userId IS NULL) instead of one OR query: the buckets don't share
5
+ // a snapshot, so a concurrent config write can mix old scope and new user values.
4
6
  export async function selectConfigRowsForScope(db, systemTenantId, tenantId, userId) {
5
7
  const [scopeRows, userRows] = await Promise.all([
6
8
  selectMany(db, configValuesTable, {
@@ -1,10 +1,11 @@
1
1
  import { requestContext } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { configuredPiiSubjectKms, encryptPiiValueForSubject, } from "@cosmicdrift/kumiko-framework/crypto";
3
3
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
4
- import { ConfigScopes, defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { ConfigScopes, defineWriteHandler, isEncryptedAtRest, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
5
5
  import { InternalError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
6
6
  import * as z from "zod";
7
7
  import { requireConfigEncryption, requireSystemDb } from "../feature.js";
8
+ import { MASKED } from "../read-redaction.js";
8
9
  import { configValueEntity, configValuesTable } from "../table.js";
9
10
  import { findConfigRow, prepareConfigWrite, resolvePiiSubject, validateBounds, validateExtensionPlugin, validatePattern, validateScope, validateType, } from "../write-helpers.js";
10
11
  const scopeEnum = z.enum([ConfigScopes.system, ConfigScopes.tenant, ConfigScopes.user]);
@@ -53,7 +54,7 @@ export const setWrite = defineWriteHandler({
53
54
  const typeError = validateType(event.payload.value, keyDef);
54
55
  if (typeError)
55
56
  return writeFailure(typeError);
56
- const pluginError = validateExtensionPlugin(ctx.registry, event.payload.key, event.payload.value);
57
+ const pluginError = validateExtensionPlugin(ctx.registry, event.payload.key, event.payload.value, keyDef);
57
58
  if (pluginError)
58
59
  return writeFailure(pluginError);
59
60
  // Bounds enforcement: hard-reject (not silent-clamp). A caller that
@@ -66,6 +67,9 @@ export const setWrite = defineWriteHandler({
66
67
  const patternError = validatePattern(event.payload.value, keyDef);
67
68
  if (patternError)
68
69
  return writeFailure(patternError);
70
+ // The batch idempotency cache stores this result and only masks entity
71
+ // writeOnly fields, so an at-rest-encrypted value must not be echoed.
72
+ const echoedValue = isEncryptedAtRest(keyDef) ? MASKED : event.payload.value;
69
73
  // backing="secrets": persist into the secrets store (system tenant, own
70
74
  // envelope encryption + audit) instead of config_values. Same JSON
71
75
  // serialization as a config row so the read path round-trips via
@@ -82,7 +86,7 @@ export const setWrite = defineWriteHandler({
82
86
  });
83
87
  return {
84
88
  isSuccess: true,
85
- data: { key: event.payload.key, value: event.payload.value, scope },
89
+ data: { key: event.payload.key, value: echoedValue, scope },
86
90
  };
87
91
  }
88
92
  let serialized = JSON.stringify(event.payload.value);
@@ -139,7 +143,7 @@ export const setWrite = defineWriteHandler({
139
143
  }
140
144
  return {
141
145
  isSuccess: true,
142
- data: { key: event.payload.key, value: event.payload.value, scope },
146
+ data: { key: event.payload.key, value: echoedValue, scope },
143
147
  };
144
148
  },
145
149
  });
@@ -9,6 +9,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
9
9
  readonly filterable?: boolean;
10
10
  readonly encrypted?: boolean;
11
11
  readonly sensitive?: boolean;
12
+ readonly writeOnly?: boolean;
12
13
  readonly readAsInstruction?: boolean;
13
14
  readonly format?: "email" | "url" | "phone" | "password";
14
15
  readonly default?: string;
@@ -29,6 +30,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
29
30
  readonly filterable?: boolean;
30
31
  readonly encrypted?: boolean;
31
32
  readonly sensitive?: boolean;
33
+ readonly writeOnly?: boolean;
32
34
  readonly readAsInstruction?: boolean;
33
35
  readonly format?: "email" | "url" | "phone" | "password";
34
36
  readonly default?: string;
@@ -49,6 +51,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
49
51
  readonly filterable?: boolean;
50
52
  readonly encrypted?: boolean;
51
53
  readonly sensitive?: boolean;
54
+ readonly writeOnly?: boolean;
52
55
  readonly readAsInstruction?: boolean;
53
56
  readonly format?: "email" | "url" | "phone" | "password";
54
57
  readonly default?: string;
@@ -71,6 +74,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
71
74
  readonly filterable?: boolean;
72
75
  readonly encrypted?: boolean;
73
76
  readonly sensitive?: boolean;
77
+ readonly writeOnly?: boolean;
74
78
  readonly readAsInstruction?: boolean;
75
79
  readonly format?: "email" | "url" | "phone" | "password";
76
80
  readonly default?: string;
@@ -91,6 +95,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
91
95
  readonly filterable?: boolean;
92
96
  readonly encrypted?: boolean;
93
97
  readonly sensitive?: boolean;
98
+ readonly writeOnly?: boolean;
94
99
  readonly readAsInstruction?: boolean;
95
100
  readonly format?: "email" | "url" | "phone" | "password";
96
101
  readonly default?: string;
@@ -111,6 +116,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
111
116
  readonly filterable?: boolean;
112
117
  readonly encrypted?: boolean;
113
118
  readonly sensitive?: boolean;
119
+ readonly writeOnly?: boolean;
114
120
  readonly readAsInstruction?: boolean;
115
121
  readonly format?: "email" | "url" | "phone" | "password";
116
122
  readonly default?: string;
@@ -8,11 +8,15 @@
8
8
  // for ITS keys (mask.title) and the per-feature group key `<feature>.settings`.
9
9
  export const defaultTranslations = {
10
10
  en: {
11
- "config.secrets.delete": "Delete",
11
+ "config.secrets.delete": "Remove",
12
+ "config.secrets.deleteConfirm": "Remove this secret? Anything using it stops working, and it cannot be restored.",
12
13
  "config.secrets.notSet": "Not set",
13
14
  "config.secrets.placeholder": "Enter a value",
14
- "config.secrets.replacePlaceholder": "Enter a new value to replace",
15
- "config.secrets.required": "Required",
15
+ "config.secrets.replacePlaceholder": "Enter a new value to replace it",
16
+ "config.secrets.saved": "Saved",
17
+ "config.secrets.section": "Secrets",
18
+ "config.secrets.description": "Stored encrypted. Saved values are never shown again.",
19
+ "config.secrets.stored": "Stored: {preview}",
16
20
  "config.secrets.set": "Set",
17
21
  "config.secrets.title": "Secrets",
18
22
  "config.settings.title": "Settings",
@@ -23,7 +27,12 @@ export const defaultTranslations = {
23
27
  "config.errors.invalidScope": "This scope is not allowed for this key.",
24
28
  "config.errors.unknownKey": "Unknown configuration key.",
25
29
  "config.errors.unknownExtensionPlugin": "This provider is not available.",
26
- "config.settings.extensionSelectorHint": "Save the provider selection to show its settings below.",
30
+ "config.settings.extensionSelectorHint": "Choose a provider and save. Its settings appear below.",
31
+ "config.settings.provider": "Provider",
32
+ "config.settings.saveProvider": "Save provider",
33
+ "config.settings.audience.system": "Applies to every tenant unless a tenant sets its own value.",
34
+ "config.settings.audience.tenant": "Applies to everyone in this tenant. Platform defaults are shown where nothing is set here.",
35
+ "config.settings.audience.user": "Only applies to you.",
27
36
  // Required by every generated screen (screenTitleKey, required-surface-keys.ts) —
28
37
  // the secrets screen has a fixed id ("secrets"), so the framework ships its
29
38
  // title translation directly instead of asking every app to declare it.
@@ -38,7 +38,7 @@ export declare function resolveScopeIds(scope: ConfigScope, tenantId: TenantId,
38
38
  userId: string | null;
39
39
  };
40
40
  export declare function resolvePiiSubject(scope: Exclude<ConfigScope, "system">, tenantId: TenantId, userId: string | null): SubjectId;
41
- export declare function validateExtensionPlugin(registry: Registry, key: string, value: string | number | boolean): KumikoError | null;
41
+ export declare function validateExtensionPlugin(registry: Pick<Registry, "getAllExtensionSelectors" | "getExtensionUsages">, key: string, value: string | number | boolean, keyDef: ConfigKeyDefinition): KumikoError | null;
42
42
  export declare function validateType(value: string | number | boolean, keyDef: ConfigKeyDefinition): KumikoError | null;
43
43
  export declare function validateBounds(value: string | number | boolean, keyDef: ConfigKeyDefinition): KumikoError | null;
44
44
  export declare function validatePattern(value: string | number | boolean, keyDef: ConfigKeyDefinition): KumikoError | null;
@@ -2,7 +2,7 @@
2
2
  // Extracted from set.write.ts so reset.write.ts + values.query.ts don't
3
3
  // have to cross-import from another handler file.
4
4
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
5
- import { ConfigScopes, extensionSelectorTargets, SYSTEM_ROLE, SYSTEM_TENANT_ID, selectablePluginIds, } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { ConfigScopes, extensionSelectorTargets, isEncryptedAtRest, SYSTEM_ROLE, SYSTEM_TENANT_ID, selectablePluginIds, } from "@cosmicdrift/kumiko-framework/engine";
6
6
  import { AccessDeniedError, InternalError, NotFoundError, UnprocessableError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
7
7
  import { assertUnreachable } from "@cosmicdrift/kumiko-framework/utils";
8
8
  import { ConfigErrors } from "./constants.js";
@@ -134,9 +134,14 @@ export function resolvePiiSubject(scope, tenantId, userId) {
134
134
  }
135
135
  return { kind: "tenant", tenantId };
136
136
  }
137
+ // An encrypted value must not travel back in an error (logs, idempotency cache),
138
+ // so rejection details spread this instead of naming `value` directly.
139
+ function echoableValue(keyDef, value) {
140
+ return isEncryptedAtRest(keyDef) ? {} : { value };
141
+ }
137
142
  // An extension-selector key may only name a mounted plugin (or "" to clear) —
138
143
  // otherwise the settings screen would show no panel for a typo'd value.
139
- export function validateExtensionPlugin(registry, key, value) {
144
+ export function validateExtensionPlugin(registry, key, value, keyDef) {
140
145
  const extensionName = extensionSelectorTargets(registry).get(key);
141
146
  if (extensionName === undefined || value === "")
142
147
  return null;
@@ -145,7 +150,7 @@ export function validateExtensionPlugin(registry, key, value) {
145
150
  return null;
146
151
  return new UnprocessableError(ConfigErrors.unknownExtensionPlugin, {
147
152
  i18nKey: "config.errors.unknownExtensionPlugin",
148
- details: { key, value, allowed },
153
+ details: { key, ...echoableValue(keyDef, value), allowed },
149
154
  });
150
155
  }
151
156
  export function validateType(value, keyDef) {
@@ -172,7 +177,7 @@ export function validateType(value, keyDef) {
172
177
  path: "value",
173
178
  code: "invalid_option",
174
179
  i18nKey: "errors.validation.invalid_option",
175
- params: { value, options: keyDef.options },
180
+ params: { ...echoableValue(keyDef, value), options: keyDef.options },
176
181
  },
177
182
  ],
178
183
  });
@@ -254,13 +259,14 @@ export function validatePattern(value, keyDef) {
254
259
  }
255
260
  if (re.test(value))
256
261
  return null;
262
+ const echoedParams = { ...echoableValue(keyDef, value), pattern: keyDef.pattern.regex };
257
263
  return new ValidationError({
258
264
  fields: [
259
265
  {
260
266
  path: "value",
261
267
  code: "invalid_format",
262
268
  i18nKey: "errors.validation.invalid_format",
263
- params: { value, pattern: keyDef.pattern.regex },
269
+ params: echoedParams,
264
270
  },
265
271
  ],
266
272
  });
@@ -133,6 +133,17 @@ getRunner) {
133
133
  });
134
134
  return effective.policy?.strategy === "blockDelete" ? deny() : undefined;
135
135
  }
136
+ // AccessDeniedError.code is the generic "access_denied" for every branch;
137
+ // the specific branch lives in details.reason.
138
+ function denialReasonOf(failure) {
139
+ const details = failure.error.details;
140
+ if (typeof details === "object" && details !== null && "reason" in details) {
141
+ const { reason } = details;
142
+ if (typeof reason === "string")
143
+ return reason;
144
+ }
145
+ return failure.error.code;
146
+ }
136
147
  // Denied cross-tenant probes must still leave an audit trail (fw#2348).
137
148
  // Appended outside the handler tx (fw#2592): the caller's
138
149
  // `return tenantScopeDenial` rolls that tx back.
@@ -228,18 +239,14 @@ export const forgetSubjectWrite = defineWriteHandler({
228
239
  const subjectKey = subjectIdToKey(subject);
229
240
  const tenantScopeDenial = await resolveTenantScopeDenial(ctx.db.unsafeRaw("tenant-scope check runs against the subject's tenant, not necessarily the caller's"), ctx.registry.features, event.user, raw);
230
241
  if (tenantScopeDenial) {
231
- const auditFailure = await appendDenialAuditEvent(ctx, event, subjectKey, raw.kind, tenantScopeDenial.error.code, ctx.dbOutsideTransaction?.unsafeRaw("denial audit append: names the prober's own tenant stream on the outside-transaction db"));
242
+ const auditFailure = await appendDenialAuditEvent(ctx, event, subjectKey, raw.kind, denialReasonOf(tenantScopeDenial), ctx.dbOutsideTransaction?.unsafeRaw("denial audit append: names the prober's own tenant stream on the outside-transaction db"));
232
243
  return auditFailure ?? tenantScopeDenial;
233
244
  }
234
- // Runs AFTER the tenant gate (VORHER only means "before the shred"): a
235
- // retention check ahead of the tenant gate would leak a foreign
236
- // entity's retention posture to a cross-tenant prober.
245
+ // Runs after the tenant gate but before any key is touched: a retention
246
+ // check ahead of the tenant gate would leak a foreign entity's retention
247
+ // posture to a cross-tenant prober.
237
248
  const retentionDenial = await resolveRetentionDenial(ctx, raw, () => ctx.db.unsafeRaw("retention check reads the record row's owning tenant, not the caller's"));
238
249
  if (retentionDenial) {
239
- // Own reason constant, not tenantScopeDenial's `.error.code` pattern:
240
- // AccessDeniedError.code is the generic "access_denied" for every
241
- // branch, so re-deriving it here would make a blockDelete refusal
242
- // indistinguishable from a cross-tenant one in the audit trail.
243
250
  const auditFailure = await appendDenialAuditEvent(ctx, event, subjectKey, raw.kind, TARGET_RECORD_RETENTION_BLOCK_DELETE, ctx.dbOutsideTransaction?.unsafeRaw("denial audit append: names the prober's own tenant stream on the outside-transaction db"));
244
251
  return auditFailure ?? retentionDenial;
245
252
  }
@@ -19,10 +19,7 @@ export const CustomFieldsHandlers = {
19
19
  export const CustomFieldsQueries = {
20
20
  fieldDefinitionList: "custom-fields:query:field-definition:list",
21
21
  };
22
- // Name the web component registers under in ExtensionSectionsProvider —
23
- // apps reference it in the screen schema via:
24
- // { kind: "extension", title, contributesToFormSubmit: true,
25
- // component: { react: { __component: CUSTOM_FIELDS_FORM_EXTENSION_NAME } } }
22
+ // Name the web component registers under in ExtensionSectionsProvider.
26
23
  export const CUSTOM_FIELDS_FORM_EXTENSION_NAME = "CustomFieldsFormSection";
27
24
  // Entity-CRUD auto-events for the `field-definition` entity. registry.ts emits
28
25
  // these as `${entityName}.created`/`.updated` (dot form) — they do NOT run
@@ -9,6 +9,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
9
9
  readonly filterable?: boolean;
10
10
  readonly encrypted?: boolean;
11
11
  readonly sensitive?: boolean;
12
+ readonly writeOnly?: boolean;
12
13
  readonly readAsInstruction?: boolean;
13
14
  readonly format?: "email" | "url" | "phone" | "password";
14
15
  readonly default?: string;
@@ -29,6 +30,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
29
30
  readonly filterable?: boolean;
30
31
  readonly encrypted?: boolean;
31
32
  readonly sensitive?: boolean;
33
+ readonly writeOnly?: boolean;
32
34
  readonly readAsInstruction?: boolean;
33
35
  readonly format?: "email" | "url" | "phone" | "password";
34
36
  readonly default?: string;
@@ -49,6 +51,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
49
51
  readonly filterable?: boolean;
50
52
  readonly encrypted?: boolean;
51
53
  readonly sensitive?: boolean;
54
+ readonly writeOnly?: boolean;
52
55
  readonly readAsInstruction?: boolean;
53
56
  readonly format?: "email" | "url" | "phone" | "password";
54
57
  readonly default?: string;
@@ -94,6 +97,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
94
97
  readonly filterable?: boolean;
95
98
  readonly encrypted?: boolean;
96
99
  readonly sensitive?: boolean;
100
+ readonly writeOnly?: boolean;
97
101
  readonly readAsInstruction?: boolean;
98
102
  readonly format?: "email" | "url" | "phone" | "password";
99
103
  readonly default?: string;
@@ -10,9 +10,8 @@ export interface ResolveForTenantArgs {
10
10
  /**
11
11
  * Layer 2 — Tenant-Preset. `undefined` (the default) means this resolver
12
12
  * derives it itself via resolveTenantRetentionPreset — one compliance-
13
- * profile read per call, fine for single-lookup callers (`policy-for`, a
14
- * per-hook TenantDb caller like notes-history-user-data). A bulk caller
15
- * iterating N entities × M tenants (the forget-cleanup runner, the
13
+ * profile read per call, fine for single-lookup callers (`policy-for`). A
14
+ * bulk caller iterating N entities × M tenants (the forget-cleanup runner, the
16
15
  * retention-cleanup cron) MUST resolve it ONCE per tenant and pass it in
17
16
  * here instead, or it re-derives per entity. `null` means "resolved
18
17
  * already, no preset applies" — the resolver falls back to Entity-Default