@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
@@ -3,5 +3,5 @@ import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
3
  export declare const LAST_TENANT_ADMIN_LOCK_NAMESPACE = 1952539757;
4
4
  type TenantDb = Parameters<typeof acquireNamespacedAdvisoryLock>[0];
5
5
  /** Refuse demoting/removing the last TenantAdmin for `tenantId`. */
6
- export declare function assertNotLastTenantAdmin(db: TenantDb, tenantId: string, userId: string): Promise<ReturnType<typeof writeFailure> | undefined>;
6
+ export declare function assertNotLastTenantAdmin(db: TenantDb, tenantId: string, userId: string, action?: "demote" | "remove"): Promise<ReturnType<typeof writeFailure> | undefined>;
7
7
  export {};
@@ -8,14 +8,16 @@ import { tenantMembershipsTable } from "./membership-table.js";
8
8
  // (dispatcher batch wraps handlers in transaction — xact lock holds through update).
9
9
  export const LAST_TENANT_ADMIN_LOCK_NAMESPACE = 0x7461646d; // 'tadm'
10
10
  /** Refuse demoting/removing the last TenantAdmin for `tenantId`. */
11
- export async function assertNotLastTenantAdmin(db, tenantId, userId) {
11
+ export async function assertNotLastTenantAdmin(db, tenantId, userId, action = "demote") {
12
12
  await acquireNamespacedAdvisoryLock(db, LAST_TENANT_ADMIN_LOCK_NAMESPACE, tenantId);
13
13
  const allMemberships = await selectMany(db, tenantMembershipsTable, { tenantId });
14
14
  const adminCount = allMemberships.filter((m) => parseRoles(m["roles"]).includes("TenantAdmin")).length;
15
15
  if (adminCount <= 1) {
16
16
  return writeFailure(new ConflictError({
17
- message: "cannot demote the last tenant admin",
18
- i18nKey: "tenant.errors.cannotDemoteLastTenantAdmin",
17
+ message: `cannot ${action} the last tenant admin`,
18
+ i18nKey: action === "remove"
19
+ ? "tenant.errors.cannotRemoveLastTenantAdmin"
20
+ : "tenant.errors.cannotDemoteLastTenantAdmin",
19
21
  details: {
20
22
  reason: TenantErrors.lastTenantAdmin,
21
23
  userId,
@@ -9,6 +9,7 @@ export declare const tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
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 tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
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;
@@ -51,6 +53,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
51
53
  readonly filterable?: boolean;
52
54
  readonly encrypted?: boolean;
53
55
  readonly sensitive?: boolean;
56
+ readonly writeOnly?: boolean;
54
57
  readonly readAsInstruction?: boolean;
55
58
  readonly format?: "email" | "url" | "phone" | "password";
56
59
  readonly default?: string;
@@ -71,6 +74,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
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;
@@ -11,6 +11,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
11
11
  readonly filterable?: boolean;
12
12
  readonly encrypted?: boolean;
13
13
  readonly sensitive?: boolean;
14
+ readonly writeOnly?: boolean;
14
15
  readonly readAsInstruction?: boolean;
15
16
  readonly format?: "email" | "url" | "phone" | "password";
16
17
  readonly default?: string;
@@ -31,6 +32,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
31
32
  readonly filterable?: boolean;
32
33
  readonly encrypted?: boolean;
33
34
  readonly sensitive?: boolean;
35
+ readonly writeOnly?: boolean;
34
36
  readonly readAsInstruction?: boolean;
35
37
  readonly format?: "email" | "url" | "phone" | "password";
36
38
  readonly default?: string;
@@ -63,7 +65,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
63
65
  readonly destroying?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
64
66
  } | undefined;
65
67
  readonly optionsQuery?: string;
66
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
68
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
67
69
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
68
70
  required: true;
69
71
  };
@@ -92,6 +94,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
92
94
  readonly filterable?: boolean;
93
95
  readonly encrypted?: boolean;
94
96
  readonly sensitive?: boolean;
97
+ readonly writeOnly?: boolean;
95
98
  readonly readAsInstruction?: boolean;
96
99
  readonly format?: "email" | "url" | "phone" | "password";
97
100
  readonly default?: string;
@@ -159,6 +162,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
159
162
  readonly filterable?: boolean;
160
163
  readonly encrypted?: boolean;
161
164
  readonly sensitive?: boolean;
165
+ readonly writeOnly?: boolean;
162
166
  readonly readAsInstruction?: boolean;
163
167
  readonly format?: "email" | "url" | "phone" | "password";
164
168
  readonly default?: string;
@@ -179,6 +183,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
179
183
  readonly filterable?: boolean;
180
184
  readonly encrypted?: boolean;
181
185
  readonly sensitive?: boolean;
186
+ readonly writeOnly?: boolean;
182
187
  readonly readAsInstruction?: boolean;
183
188
  readonly format?: "email" | "url" | "phone" | "password";
184
189
  readonly default?: string;
@@ -211,7 +216,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
211
216
  readonly destroying?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
212
217
  } | undefined;
213
218
  readonly optionsQuery?: string;
214
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
219
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
215
220
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
216
221
  required: true;
217
222
  };
@@ -240,6 +245,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
240
245
  readonly filterable?: boolean;
241
246
  readonly encrypted?: boolean;
242
247
  readonly sensitive?: boolean;
248
+ readonly writeOnly?: boolean;
243
249
  readonly readAsInstruction?: boolean;
244
250
  readonly format?: "email" | "url" | "phone" | "password";
245
251
  readonly default?: string;
@@ -38,6 +38,10 @@ export declare function fireEntityPostSave(hooks: SeedTenantHooks | undefined, p
38
38
  * a second call for the same `id` is a no-op (no update path). Same
39
39
  * TX-semantics as the real `TenantHandlers.create`, minus the SystemAdmin-
40
40
  * access-check and minus ConflictError-on-duplicate.
41
+ *
42
+ * `hooks` run only for the call that actually creates the tenant: a call that
43
+ * finds the tenant/stream already present (or loses a concurrent create race)
44
+ * returns without firing postSave.
41
45
  */
42
46
  export declare function seedTenant(db: DbRunner, options: SeedTenantOptions, hooks?: SeedTenantHooks): Promise<{
43
47
  id: TenantId;
@@ -124,6 +124,10 @@ target) {
124
124
  * a second call for the same `id` is a no-op (no update path). Same
125
125
  * TX-semantics as the real `TenantHandlers.create`, minus the SystemAdmin-
126
126
  * access-check and minus ConflictError-on-duplicate.
127
+ *
128
+ * `hooks` run only for the call that actually creates the tenant: a call that
129
+ * finds the tenant/stream already present (or loses a concurrent create race)
130
+ * returns without firing postSave.
127
131
  */
128
132
  export async function seedTenant(db, options, hooks) {
129
133
  const by = options.by ?? TestUsers.systemAdmin;
@@ -146,8 +150,11 @@ export async function seedTenant(db, options, hooks) {
146
150
  const result = await tenantExecutor.create({ id: options.id, key: options.key, name: options.name }, by, tdb);
147
151
  if (!result.isSuccess) {
148
152
  // Same idempotency case as the stream-version check above, only detected
149
- // one instruction later: a dispatcher cycle appended to the stream between
150
- // that read and this create. Both paths return without firing postSave.
153
+ // one instruction later: a concurrent writer (e.g. a parallel seedTenant
154
+ // for the same id) created the stream between the version read above and
155
+ // this create; the executor confines the failed append to a savepoint, so
156
+ // the surrounding TX stays usable. Both paths return without firing
157
+ // postSave — hooks only run for the call that actually created the tenant.
151
158
  if (result.error.code === "version_conflict")
152
159
  return { id: options.id };
153
160
  throw new Error(`seedTenant failed: ${result.error.code} — ${JSON.stringify(result.error.details ?? {})}`);
@@ -1,2 +1,3 @@
1
1
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare function validateTenantDataHookCoverage(features: readonly FeatureDefinition[]): void;
3
+ export declare function validateTenantDestroyHookShapes(features: readonly FeatureDefinition[]): void;
@@ -1,4 +1,4 @@
1
- import { EXT_TENANT_DATA, } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { EXT_EXTERNAL_RESOURCE, EXT_INFRA_RESOURCE, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, isTenantDataExtensionHooks, isTenantResourceExtensionHooks, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { entitiesOf } from "../shared/index.js";
3
3
  // V4: tenantOwned-entity-without-hook gate. Mirrors user-data-rights' V3
4
4
  // (validateGdprPiiHookCoverage) but for EXT_TENANT_DATA. Registered as this
@@ -34,3 +34,29 @@ export function validateTenantDataHookCoverage(features) {
34
34
  }
35
35
  }
36
36
  }
37
+ const TENANT_RESOURCE_EXTENSIONS = new Set([
38
+ EXT_STORAGE_PROVIDER,
39
+ EXT_SEARCH_ADAPTER,
40
+ EXT_EXTERNAL_RESOURCE,
41
+ EXT_INFRA_RESOURCE,
42
+ ]);
43
+ // Malformed destroy registrations would otherwise only throw when a tenant
44
+ // destruction reaches the stage, possibly months after deploy, and stall the
45
+ // whole pipeline (stages.ts keeps its own throw as defense in depth). JS
46
+ // plugins and `as` casts slip past the type check.
47
+ export function validateTenantDestroyHookShapes(features) {
48
+ for (const feature of features) {
49
+ for (const usage of feature.extensionUsages) {
50
+ const isTenantData = usage.extensionName === EXT_TENANT_DATA;
51
+ if (!isTenantData && !TENANT_RESOURCE_EXTENSIONS.has(usage.extensionName))
52
+ continue;
53
+ const isValid = isTenantData
54
+ ? isTenantDataExtensionHooks(usage.options)
55
+ : isTenantResourceExtensionHooks(usage.options);
56
+ if (isValid)
57
+ continue;
58
+ const requiredFn = isTenantData ? "destroy" : "destroyTenant";
59
+ throw new Error(`[kumiko:boot] ${usage.extensionName} registration for "${usage.entityName}" (feature "${feature.name}") has no ${requiredFn} function — tenant destroy would fail at this stage.`);
60
+ }
61
+ }
62
+ }
@@ -1,5 +1,5 @@
1
1
  import { defineFeature, EXT_EXTERNAL_RESOURCE, EXT_INFRA_RESOURCE, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, } from "@cosmicdrift/kumiko-framework/engine";
2
- import { validateTenantDataHookCoverage } from "./boot-checks.js";
2
+ import { validateTenantDataHookCoverage, validateTenantDestroyHookShapes } from "./boot-checks.js";
3
3
  import { DESTRUCTION_CANCELLED_EVENT_SHORT, DESTRUCTION_REQUESTED_EVENT_SHORT, TENANT_DESTRUCTION_COMPLETED_EVENT_SHORT, TENANT_DESTRUCTION_FAILED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_ABANDONED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_FAILED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_STARTED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_SUCCEEDED_EVENT_SHORT, TENANT_DESTRUCTION_STARTED_EVENT_SHORT, } from "./constants.js";
4
4
  import { destructionCancelledSchema, destructionRequestedSchema, tenantDestructionCompletedSchema, tenantDestructionFailedSchema, tenantDestructionStageAbandonedSchema, tenantDestructionStageFailedSchema, tenantDestructionStageStartedSchema, tenantDestructionStageSucceededSchema, tenantDestructionStartedSchema, } from "./events.js";
5
5
  import { cancelDestructionWrite } from "./handlers/cancel-destruction.write.js";
@@ -36,6 +36,7 @@ export function createTenantLifecycleFeature() {
36
36
  // EXT_TENANT_DATA: its own mount is the trigger, matching the original
37
37
  // guard's "tenant-lifecycle mounted" gate exactly.
38
38
  r.bootCheck(({ features }) => validateTenantDataHookCoverage(features));
39
+ r.bootCheck(({ features }) => validateTenantDestroyHookShapes(features));
39
40
  r.defineEvent(DESTRUCTION_REQUESTED_EVENT_SHORT, destructionRequestedSchema, {
40
41
  piiFields: "none",
41
42
  });
@@ -2,7 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { configuredPiiSubjectKms } from "@cosmicdrift/kumiko-framework/crypto";
3
3
  import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
4
4
  import { createSystemUser, EXT_EXTERNAL_RESOURCE, EXT_INFRA_RESOURCE, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, extensionUsageEscapeHatchReason, isTenantDataExtensionHooks, isTenantResourceExtensionHooks, } from "@cosmicdrift/kumiko-framework/engine";
5
- import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
5
+ import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
6
6
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
7
7
  import { tenantEntity, tenantMembershipEntity, tenantMembershipsTable, tenantTable, } from "../tenant/index.js";
8
8
  import { invalidateTenantLifecycleGate } from "./lifecycle-gate.js";
@@ -30,7 +30,7 @@ async function runTenantDataHooks(ctx) {
30
30
  const report = createEscapeHatchReporter({
31
31
  handler: `${EXT_TENANT_DATA}:${usage.entityName}`,
32
32
  tenantId: ctx.tenantId,
33
- actor: ctx.actor ?? "system",
33
+ actor: ctx.actor ?? UNATTRIBUTED_ACTOR,
34
34
  sink: ctx.escapeHatchAuditSink,
35
35
  });
36
36
  const hookCtx = {
@@ -21,21 +21,24 @@ export function createTenantSettingsFeature(opts = {}) {
21
21
  r.config({ keys: buildTenantSettingsKeys(opts) });
22
22
  r.translations({
23
23
  keys: {
24
- "tenant-settings.currency": { en: "Default Currency" },
25
- "tenant-settings.locale": { en: "Default Locale" },
24
+ "tenant-settings.currency": { en: "Default currency" },
25
+ "tenant-settings.locale": { en: "Default locale" },
26
26
  // Settings-Hub-derived configEdit screen title (buildConfigFeatureSchema) —
27
27
  // required by the i18n boot-validator whenever a feature masks config keys.
28
- "screen:tenant-settings-system.title": { en: "Tenant Settings" },
29
- "screen:tenant-settings-tenant.title": { en: "Tenant Settings" },
28
+ "screen:tenant-settings-system.title": { en: "Tenant defaults" },
29
+ "screen:tenant-settings-tenant.title": { en: "Tenant settings" },
30
30
  // Settings-Hub nav label + configEdit section heading (same key, both
31
31
  // scopes fall back here — see buildConfigFeatureSchema).
32
- "tenant-settings.settings": { en: "Tenant Settings" },
32
+ "tenant-settings.settings": { en: "Tenant settings" },
33
+ // Section heading below the page title (the page title is the same words as
34
+ // `.settings`, which RenderEdit would drop as a repeat).
35
+ "tenant-settings.settings.section": { en: "Currency and language" },
33
36
  // Both currency/locale are tenant-home with admin write access, which
34
37
  // cascades a SystemAdmin-only "set the platform default" screen up to
35
38
  // the system audience — scoped override so its nav entry reads
36
39
  // differently from the tenant-home entry instead of repeating the
37
40
  // same words under both "Platform" and "Organization".
38
- "tenant-settings.settings.system": { en: "Tenant Settings (Platform Default)" },
41
+ "tenant-settings.settings.system": { en: "Tenant defaults" },
39
42
  },
40
43
  });
41
44
  });
@@ -9,6 +9,7 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
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 tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
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;
@@ -129,11 +129,12 @@ export function createTierEngineFeature(opts = {}) {
129
129
  // Standard-CRUD via Helper.
130
130
  r.writeHandler(defineEntityCreateHandler("tier-assignment", tierAssignmentEntity, {
131
131
  ...writeAccess,
132
- description: "Creates the tier-assignment row of the caller's tenant with a tier name and its source; prefer `set-tenant-tier`, which upserts and refreshes the feature gate.",
132
+ // Bypasses the feature-gate refresh that set-tenant-tier performs.
133
+ agent: { expose: false },
133
134
  }));
134
135
  r.writeHandler(defineEntityUpdateHandler("tier-assignment", tierAssignmentEntity, {
135
136
  ...writeAccess,
136
- description: "Updates an existing tier-assignment row by id; prefer `set-tenant-tier`, which resolves the tenant's row itself and refreshes the feature gate.",
137
+ agent: { expose: false },
137
138
  }));
138
139
  // Reads.
139
140
  r.queryHandler(defineEntityListHandler("tier-assignment", tierAssignmentEntity, {
@@ -17,6 +17,7 @@ export declare function createTierResolver<TTier extends string, TCaps>(deps: Ti
17
17
  readonly filterable?: boolean;
18
18
  readonly encrypted?: boolean;
19
19
  readonly sensitive?: boolean;
20
+ readonly writeOnly?: boolean;
20
21
  readonly readAsInstruction?: boolean;
21
22
  readonly format?: "email" | "url" | "phone" | "password";
22
23
  readonly default?: string;
@@ -37,6 +38,7 @@ export declare function createTierResolver<TTier extends string, TCaps>(deps: Ti
37
38
  readonly filterable?: boolean;
38
39
  readonly encrypted?: boolean;
39
40
  readonly sensitive?: boolean;
41
+ readonly writeOnly?: boolean;
40
42
  readonly readAsInstruction?: boolean;
41
43
  readonly format?: "email" | "url" | "phone" | "password";
42
44
  readonly default?: string;
@@ -3,7 +3,7 @@ export declare const UserCommandSchemas: {
3
3
  email: import("zod").ZodEmail;
4
4
  passwordHash: import("zod").ZodOptional<import("zod").ZodString>;
5
5
  displayName: import("zod").ZodString;
6
- locale: import("zod").ZodOptional<import("zod").ZodString>;
6
+ locale: import("zod").ZodOptional<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<string, string>>>;
7
7
  timezone: import("zod").ZodOptional<import("zod").ZodString>;
8
8
  roles: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodArray<import("zod").ZodString>, import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<string[], string>>]>>;
9
9
  }, import("zod/v4/core").$strip>;
@@ -12,7 +12,7 @@ export declare const UserCommandSchemas: {
12
12
  version: import("zod").ZodNumber;
13
13
  changes: import("zod").ZodObject<{
14
14
  displayName: import("zod").ZodOptional<import("zod").ZodString>;
15
- locale: import("zod").ZodOptional<import("zod").ZodString>;
15
+ locale: import("zod").ZodOptional<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<string, string>>>;
16
16
  timezone: import("zod").ZodOptional<import("zod").ZodString>;
17
17
  email: import("zod").ZodOptional<import("zod").ZodEmail>;
18
18
  passwordHash: import("zod").ZodOptional<import("zod").ZodString>;
@@ -3,7 +3,7 @@ export declare const createWrite: import("@cosmicdrift/kumiko-types/define-handl
3
3
  email: z.ZodEmail;
4
4
  passwordHash: z.ZodOptional<z.ZodString>;
5
5
  displayName: z.ZodString;
6
- locale: z.ZodOptional<z.ZodString>;
6
+ locale: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
7
7
  timezone: z.ZodOptional<z.ZodString>;
8
8
  roles: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>]>>;
9
9
  }, z.core.$strip>, import("@cosmicdrift/kumiko-types/hooks").SaveContext, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -2,7 +2,7 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { defineWriteHandler, findForbiddenRoleAssignment, } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { AccessDeniedError, ConflictError, InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
5
- import { isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
5
+ import { canonicalizeLocaleTag, isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
6
6
  import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
7
7
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
8
8
  import * as z from "zod";
@@ -28,7 +28,13 @@ export const createWrite = defineWriteHandler({
28
28
  email: z.email(),
29
29
  passwordHash: z.string().optional(),
30
30
  displayName: z.string().min(1).max(100),
31
- locale: z.string().min(2).max(10).refine(isValidLocaleTag, "invalid locale tag").optional(),
31
+ locale: z
32
+ .string()
33
+ .min(2)
34
+ .max(10)
35
+ .refine(isValidLocaleTag, "invalid locale tag")
36
+ .transform(canonicalizeLocaleTag)
37
+ .optional(),
32
38
  timezone: z.string().max(64).refine(isValidIanaTimeZone, "invalid IANA time zone").optional(),
33
39
  // Field-level write access (privileged) is defense-in-depth — create is
34
40
  // already system/SystemAdmin-only.
@@ -4,7 +4,7 @@ export declare const updateWrite: import("@cosmicdrift/kumiko-types/define-handl
4
4
  version: z.ZodNumber;
5
5
  changes: z.ZodObject<{
6
6
  displayName: z.ZodOptional<z.ZodString>;
7
- locale: z.ZodOptional<z.ZodString>;
7
+ locale: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
8
8
  timezone: z.ZodOptional<z.ZodString>;
9
9
  email: z.ZodOptional<z.ZodEmail>;
10
10
  passwordHash: z.ZodOptional<z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { access, defineWriteHandler, hasAccess } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { AccessDeniedError, InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
4
- import { isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
4
+ import { canonicalizeLocaleTag, isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
5
5
  import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
6
6
  import * as z from "zod";
7
7
  import { UserErrors } from "../constants.js";
@@ -24,7 +24,13 @@ export const updateWrite = defineWriteHandler({
24
24
  version: z.number(),
25
25
  changes: z.object({
26
26
  displayName: z.string().min(1).max(100).optional(),
27
- locale: z.string().min(2).max(10).refine(isValidLocaleTag, "invalid locale tag").optional(),
27
+ locale: z
28
+ .string()
29
+ .min(2)
30
+ .max(10)
31
+ .refine(isValidLocaleTag, "invalid locale tag")
32
+ .transform(canonicalizeLocaleTag)
33
+ .optional(),
28
34
  timezone: z.string().max(64).refine(isValidIanaTimeZone, "invalid IANA time zone").optional(),
29
35
  email: z.email().optional(),
30
36
  passwordHash: z.string().optional(),
@@ -38,6 +38,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
38
38
  readonly filterable?: boolean;
39
39
  readonly encrypted?: boolean;
40
40
  readonly sensitive?: boolean;
41
+ readonly writeOnly?: boolean;
41
42
  readonly readAsInstruction?: boolean;
42
43
  readonly format?: "email" | "url" | "phone" | "password";
43
44
  readonly default?: string;
@@ -58,6 +59,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
58
59
  readonly filterable?: boolean;
59
60
  readonly encrypted?: boolean;
60
61
  readonly sensitive?: boolean;
62
+ readonly writeOnly?: boolean;
61
63
  readonly readAsInstruction?: boolean;
62
64
  readonly format?: "email" | "url" | "phone" | "password";
63
65
  readonly default?: string;
@@ -78,6 +80,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
78
80
  readonly filterable?: boolean;
79
81
  readonly encrypted?: boolean;
80
82
  readonly sensitive?: boolean;
83
+ readonly writeOnly?: boolean;
81
84
  readonly readAsInstruction?: boolean;
82
85
  readonly format?: "email" | "url" | "phone" | "password";
83
86
  readonly default?: string;
@@ -98,6 +101,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
98
101
  readonly filterable?: boolean;
99
102
  readonly encrypted?: boolean;
100
103
  readonly sensitive?: boolean;
104
+ readonly writeOnly?: boolean;
101
105
  readonly readAsInstruction?: boolean;
102
106
  readonly format?: "email" | "url" | "phone" | "password";
103
107
  readonly default?: string;
@@ -127,6 +131,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
127
131
  readonly filterable?: boolean;
128
132
  readonly encrypted?: boolean;
129
133
  readonly sensitive?: boolean;
134
+ readonly writeOnly?: boolean;
130
135
  readonly readAsInstruction?: boolean;
131
136
  readonly format?: "email" | "url" | "phone" | "password";
132
137
  readonly default?: string;
@@ -159,7 +164,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
159
164
  readonly restricted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
160
165
  } | undefined;
161
166
  readonly optionsQuery?: string;
162
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
167
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
163
168
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
164
169
  required: true;
165
170
  };
@@ -188,6 +193,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
188
193
  readonly filterable?: boolean;
189
194
  readonly encrypted?: boolean;
190
195
  readonly sensitive?: boolean;
196
+ readonly writeOnly?: boolean;
191
197
  readonly readAsInstruction?: boolean;
192
198
  readonly format?: "email" | "url" | "phone" | "password";
193
199
  readonly default?: string;
@@ -210,6 +216,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
210
216
  readonly filterable?: boolean;
211
217
  readonly encrypted?: boolean;
212
218
  readonly sensitive?: boolean;
219
+ readonly writeOnly?: boolean;
213
220
  readonly readAsInstruction?: boolean;
214
221
  readonly format?: "email" | "url" | "phone" | "password";
215
222
  readonly default?: string;
@@ -230,6 +237,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
230
237
  readonly filterable?: boolean;
231
238
  readonly encrypted?: boolean;
232
239
  readonly sensitive?: boolean;
240
+ readonly writeOnly?: boolean;
233
241
  readonly readAsInstruction?: boolean;
234
242
  readonly format?: "email" | "url" | "phone" | "password";
235
243
  readonly default?: string;
@@ -250,6 +258,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
250
258
  readonly filterable?: boolean;
251
259
  readonly encrypted?: boolean;
252
260
  readonly sensitive?: boolean;
261
+ readonly writeOnly?: boolean;
253
262
  readonly readAsInstruction?: boolean;
254
263
  readonly format?: "email" | "url" | "phone" | "password";
255
264
  readonly default?: string;
@@ -270,6 +279,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
270
279
  readonly filterable?: boolean;
271
280
  readonly encrypted?: boolean;
272
281
  readonly sensitive?: boolean;
282
+ readonly writeOnly?: boolean;
273
283
  readonly readAsInstruction?: boolean;
274
284
  readonly format?: "email" | "url" | "phone" | "password";
275
285
  readonly default?: string;
@@ -299,6 +309,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
299
309
  readonly filterable?: boolean;
300
310
  readonly encrypted?: boolean;
301
311
  readonly sensitive?: boolean;
312
+ readonly writeOnly?: boolean;
302
313
  readonly readAsInstruction?: boolean;
303
314
  readonly format?: "email" | "url" | "phone" | "password";
304
315
  readonly default?: string;
@@ -331,7 +342,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
331
342
  readonly restricted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
332
343
  } | undefined;
333
344
  readonly optionsQuery?: string;
334
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
345
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
335
346
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
336
347
  required: true;
337
348
  };
@@ -360,6 +371,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
360
371
  readonly filterable?: boolean;
361
372
  readonly encrypted?: boolean;
362
373
  readonly sensitive?: boolean;
374
+ readonly writeOnly?: boolean;
363
375
  readonly readAsInstruction?: boolean;
364
376
  readonly format?: "email" | "url" | "phone" | "password";
365
377
  readonly default?: string;
@@ -8,5 +8,7 @@ export type ExportJobCleanupCandidate = {
8
8
  readonly requestedFromTenantId: TenantId;
9
9
  readonly downloadStorageKey: string | null;
10
10
  readonly expiresAt: Temporal.Instant | null;
11
+ readonly completedAt: Temporal.Instant | null;
12
+ readonly startedAt: Temporal.Instant | null;
11
13
  };
12
14
  export declare function selectExportJobsForStorageCleanup(db: DbConnection, doneStatus: string, failedStatus: string): Promise<readonly ExportJobCleanupCandidate[]>;
@@ -25,7 +25,7 @@ import { isMailTransportAvailable, makeDefaultDeletionExecutedEmail, makeDefault
25
25
  import { makeTenantMailTransportResolver } from "./lib/mail-transport-resolver.js";
26
26
  import { resolveAppTenantModel } from "./lib/resolve-tenant-model.js";
27
27
  import { makeTenantStorageProviderResolver } from "./lib/storage-provider-resolver.js";
28
- import { EXPORT_CLEANUP_BACKLOG_AGE_METRIC, runExportJobs, } from "./run-export-jobs.js";
28
+ import { EXPORT_CLEANUP_BACKLOG_AGE_METRIC, EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC, runExportJobs, } from "./run-export-jobs.js";
29
29
  import { runForgetCleanup, sanitizeReasonForLog, } from "./run-forget-cleanup.js";
30
30
  import { downloadAttemptEntity } from "./schema/download-attempt.js";
31
31
  import { exportDownloadTokenEntity } from "./schema/download-token.js";
@@ -156,12 +156,18 @@ export function createUserDataRightsFeature(opts = {}) {
156
156
  "userDataRights.privacyCenter.field.gracePeriodEnd": { en: "Scheduled deletion date" },
157
157
  "userDataRights.privacyCenter.export.title": { en: "Export your data (Art. 20)" },
158
158
  "userDataRights.privacyCenter.restriction.title": { en: "Restrict processing (Art. 18)" },
159
+ "userDataRights.privacyCenter.restriction.explainer": {
160
+ en: "Freeze your account: processing of your data is paused and you are signed out. Lifting the restriction afterwards is only possible via support.",
161
+ },
159
162
  "userDataRights.privacyCenter.restriction.restrict": { en: "Restrict account" },
160
163
  "userDataRights.privacyCenter.restriction.dialogTitle": { en: "Restrict your account?" },
161
164
  "userDataRights.privacyCenter.restriction.dialogDescription": {
162
165
  en: "You will be signed out immediately and cannot sign in again until support lifts the restriction.",
163
166
  },
164
167
  "userDataRights.privacyCenter.deletion.title": { en: "Delete account (Art. 17)" },
168
+ "userDataRights.privacyCenter.deletion.explainer": {
169
+ en: "Request deletion of your account. Until the grace period ends you can cancel the deletion.",
170
+ },
165
171
  "userDataRights.privacyCenter.deletion.delete": { en: "Delete account" },
166
172
  "userDataRights.privacyCenter.deletion.cancel": { en: "Cancel deletion" },
167
173
  "userDataRights.privacyCenter.deletion.dialogTitle": { en: "Delete your account?" },
@@ -215,7 +221,7 @@ export function createUserDataRightsFeature(opts = {}) {
215
221
  // when `showDeletion` is false, and status covers restriction
216
222
  // AND deletion state, so it can't hang off the optional section.
217
223
  title: i18nKey("userDataRights.privacyCenter.restriction.title"),
218
- description: "userDataRights.privacyCenter.restriction.explainer",
224
+ description: i18nKey("userDataRights.privacyCenter.restriction.explainer"),
219
225
  fields: [
220
226
  {
221
227
  field: "status",
@@ -227,7 +233,7 @@ export function createUserDataRightsFeature(opts = {}) {
227
233
  ? [
228
234
  {
229
235
  title: i18nKey("userDataRights.privacyCenter.deletion.title"),
230
- description: "userDataRights.privacyCenter.deletion.explainer",
236
+ description: i18nKey("userDataRights.privacyCenter.deletion.explainer"),
231
237
  // gracePeriodEnd is only meaningful once a deletion is
232
238
  // actually pending — hide it instead of showing an empty
233
239
  // date when status isn't deletionRequested.
@@ -269,6 +275,8 @@ export function createUserDataRightsFeature(opts = {}) {
269
275
  handler: UserDataRightsHandlers.cancelDeletion,
270
276
  visible: { field: "status", eq: "deletionRequested" },
271
277
  style: "secondary",
278
+ // Declared: the id's last segment ("deletion") would otherwise resolve to the destructive trash icon.
279
+ icon: "x",
272
280
  },
273
281
  ]
274
282
  : []),
@@ -372,15 +380,20 @@ export function createUserDataRightsFeature(opts = {}) {
372
380
  // Surfaces a stalled export-cleanup cron: if the daily
373
381
  // pass stops running, done-status export bundles keep sitting in
374
382
  // storage past their TTL+grace with downloadStorageKey still set
375
- // (unencrypted PII at rest). Emitted from the same job that cleans up
376
- // (see handler below) — a dead cron makes the metric go stale too,
377
- // which an `absent()` alert catches without a second liveness signal.
378
- // Scope: Done-status bundles only — Failed-status cleanup candidates
379
- // have no expiresAt/TTL (immediate cleanup, see storageCleanupPass).
383
+ // (unencrypted PII at rest). Gauges keep their last value in-process, so
384
+ // a dead cron does NOT make the backlog gauge absent; the last-run
385
+ // timestamp gauge below is the liveness signal.
386
+ // Scope: Done-status bundles past expiresAt+grace plus Failed-status
387
+ // orphans (no TTL, due from completedAt ?? startedAt; see storageCleanupPass).
380
388
  r.metric(EXPORT_CLEANUP_BACKLOG_AGE_METRIC, {
381
389
  type: "gauge",
382
390
  unit: "seconds",
383
- description: "Age in seconds of the oldest done-status export bundle whose expiresAt+grace has passed but downloadStorageKey is still set. 0 when the storage-cleanup pass has no backlog.",
391
+ description: "Age in seconds of the oldest export bundle (done: expiresAt+grace passed; failed: orphaned ZIP since completedAt or startedAt) whose downloadStorageKey is still set. 0 when the storage-cleanup pass has no backlog.",
392
+ });
393
+ r.metric(EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC, {
394
+ type: "gauge",
395
+ unit: "seconds",
396
+ description: "Unix timestamp of the last export storage-cleanup pass that completed. Alert on time() minus this value exceeding the cron interval plus a buffer; the backlog-age gauge alone stays at its last value when the cron stops.",
384
397
  });
385
398
  // S2.U3 Atom 3b — Worker fuer Async Export-Pipeline. Cron-getriggert.
386
399
  const RUN_EXPORT_JOBS_REASON = "processes pending export jobs across every tenant";