@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
@@ -1,3 +1,4 @@
1
+ import type { EntityAuditColumns } from "./table.js";
1
2
  export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
2
3
  ownerId: {
3
4
  readonly type: "text";
@@ -9,6 +10,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
9
10
  readonly filterable?: boolean;
10
11
  readonly encrypted?: boolean;
11
12
  readonly sensitive?: boolean;
13
+ readonly writeOnly?: boolean;
12
14
  readonly readAsInstruction?: boolean;
13
15
  readonly format?: "email" | "url" | "phone" | "password";
14
16
  readonly default?: string;
@@ -29,6 +31,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
29
31
  readonly filterable?: boolean;
30
32
  readonly encrypted?: boolean;
31
33
  readonly sensitive?: boolean;
34
+ readonly writeOnly?: boolean;
32
35
  readonly readAsInstruction?: boolean;
33
36
  readonly format?: "email" | "url" | "phone" | "password";
34
37
  readonly default?: string;
@@ -59,7 +62,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
59
62
  readonly "text-block"?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
60
63
  } | undefined;
61
64
  readonly optionsQuery?: string;
62
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
65
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
63
66
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
64
67
  required: true;
65
68
  };
@@ -73,6 +76,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
73
76
  readonly filterable?: boolean;
74
77
  readonly encrypted?: boolean;
75
78
  readonly sensitive?: boolean;
79
+ readonly writeOnly?: boolean;
76
80
  readonly readAsInstruction?: boolean;
77
81
  readonly format?: "email" | "url" | "phone" | "password";
78
82
  readonly default?: string;
@@ -93,6 +97,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
93
97
  readonly filterable?: boolean;
94
98
  readonly encrypted?: boolean;
95
99
  readonly sensitive?: boolean;
100
+ readonly writeOnly?: boolean;
96
101
  readonly readAsInstruction?: boolean;
97
102
  readonly format?: "email" | "url" | "phone" | "password";
98
103
  readonly default?: string;
@@ -113,6 +118,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
113
118
  readonly filterable?: boolean;
114
119
  readonly encrypted?: boolean;
115
120
  readonly sensitive?: boolean;
121
+ readonly writeOnly?: boolean;
116
122
  readonly readAsInstruction?: boolean;
117
123
  readonly format?: "email" | "url" | "phone" | "password";
118
124
  readonly default?: string;
@@ -157,7 +163,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
157
163
  readonly plain?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
158
164
  } | undefined;
159
165
  readonly optionsQuery?: string;
160
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
166
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
161
167
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
162
168
  required: true;
163
169
  };
@@ -173,6 +179,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
173
179
  readonly filterable?: boolean;
174
180
  readonly encrypted?: boolean;
175
181
  readonly sensitive?: boolean;
182
+ readonly writeOnly?: boolean;
176
183
  readonly readAsInstruction?: boolean;
177
184
  readonly format?: "email" | "url" | "phone" | "password";
178
185
  readonly default?: string;
@@ -193,6 +200,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
193
200
  readonly filterable?: boolean;
194
201
  readonly encrypted?: boolean;
195
202
  readonly sensitive?: boolean;
203
+ readonly writeOnly?: boolean;
196
204
  readonly readAsInstruction?: boolean;
197
205
  readonly format?: "email" | "url" | "phone" | "password";
198
206
  readonly default?: string;
@@ -223,7 +231,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
223
231
  readonly "text-block"?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
224
232
  } | undefined;
225
233
  readonly optionsQuery?: string;
226
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
234
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
227
235
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
228
236
  required: true;
229
237
  };
@@ -237,6 +245,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
237
245
  readonly filterable?: boolean;
238
246
  readonly encrypted?: boolean;
239
247
  readonly sensitive?: boolean;
248
+ readonly writeOnly?: boolean;
240
249
  readonly readAsInstruction?: boolean;
241
250
  readonly format?: "email" | "url" | "phone" | "password";
242
251
  readonly default?: string;
@@ -257,6 +266,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
257
266
  readonly filterable?: boolean;
258
267
  readonly encrypted?: boolean;
259
268
  readonly sensitive?: boolean;
269
+ readonly writeOnly?: boolean;
260
270
  readonly readAsInstruction?: boolean;
261
271
  readonly format?: "email" | "url" | "phone" | "password";
262
272
  readonly default?: string;
@@ -277,6 +287,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
277
287
  readonly filterable?: boolean;
278
288
  readonly encrypted?: boolean;
279
289
  readonly sensitive?: boolean;
290
+ readonly writeOnly?: boolean;
280
291
  readonly readAsInstruction?: boolean;
281
292
  readonly format?: "email" | "url" | "phone" | "password";
282
293
  readonly default?: string;
@@ -321,7 +332,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
321
332
  readonly plain?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
322
333
  } | undefined;
323
334
  readonly optionsQuery?: string;
324
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
335
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
325
336
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
326
337
  required: true;
327
338
  };
@@ -338,8 +349,4 @@ export type UserContentEntryRow = {
338
349
  readonly folder: string | null;
339
350
  readonly content: string | null;
340
351
  readonly contentFormat: string;
341
- readonly createdAt: Date;
342
- readonly updatedAt: Date;
343
- readonly createdBy: string;
344
- readonly updatedBy: string;
345
- };
352
+ } & EntityAuditColumns;
@@ -6,7 +6,7 @@ export type BlockSummary = {
6
6
  readonly title: string | null;
7
7
  readonly content: string | null;
8
8
  readonly folder: string | null;
9
- readonly updatedAt: string;
9
+ readonly modifiedAt: string | null;
10
10
  };
11
11
  export declare function groupBlocksByFolder(blocks: readonly BlockSummary[], tenantIdOverride?: string, collectionId?: string): readonly TreeNode[];
12
12
  export declare function textBlocksClient(opts?: {
@@ -21,7 +21,7 @@ export const userContentExportHook = async (ctx) => {
21
21
  title: r["title"],
22
22
  content: r["content"],
23
23
  folder: r["folder"],
24
- updatedAt: String(r["updatedAt"] ?? ""),
24
+ modifiedAt: String(r["modifiedAt"] ?? ""),
25
25
  })),
26
26
  };
27
27
  };
@@ -0,0 +1,8 @@
1
+ import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import type { Redis } from "ioredis";
5
+ export declare function cancelPendingInvitation(db: TenantDb, invitation: {
6
+ readonly id: string;
7
+ readonly version: number;
8
+ }, actor: SessionUser, redis: Redis | undefined): Promise<WriteFailure | undefined>;
@@ -0,0 +1,22 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ // kumiko-lint-ignore cross-feature-import cancel needs invite-token-store for Redis cleanup
3
+ import { invalidateExistingInviteToken } from "../auth-email-password/invite-token-store.js";
4
+ import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "./invitation-table.js";
5
+ const invitationExecutor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, { entityName: "tenant-invitation" });
6
+ // Shared by cancel-invitation and remove-member so the cancel behavior
7
+ // (status transition + token invalidation) cannot drift between them.
8
+ export async function cancelPendingInvitation(db, invitation, actor, redis) {
9
+ const updateResult = await invitationExecutor.update({
10
+ id: invitation.id,
11
+ version: invitation.version,
12
+ changes: { status: INVITATION_STATUS.cancelled },
13
+ }, actor, db);
14
+ if (!updateResult.isSuccess)
15
+ return updateResult;
16
+ // Delete the token from Redis (if still there). If Redis is
17
+ // unavailable or the token already expired: not a problem, the DB
18
+ // row is the single source of truth for the UI.
19
+ if (redis)
20
+ await invalidateExistingInviteToken(redis, invitation.id);
21
+ return undefined;
22
+ }
@@ -121,7 +121,8 @@ export function createTenantFeature(options) {
121
121
  // for existing callers). Cross-tenant because the feature is systemScope.
122
122
  r.queryHandler(defineEntityListHandler("tenant", tenantEntity, {
123
123
  access: { roles: ["SystemAdmin"] },
124
- description: "Lists tenants under the entity-convention name the generated SystemAdmin tenant-list screen resolves; tenant:query:list is the hand-written equivalent.",
124
+ // Duplicate of tenant:query:list for agents.
125
+ agent: { expose: false },
125
126
  }));
126
127
  r.queryHandler(defineEntityDetailHandler("tenant", tenantEntity, {
127
128
  access: { roles: ["SystemAdmin"] },
@@ -129,7 +130,8 @@ export function createTenantFeature(options) {
129
130
  }));
130
131
  r.writeHandler(defineEntityUpdateHandler("tenant", tenantEntity, {
131
132
  access: { roles: ["SystemAdmin"] },
132
- description: "Saves edits to a tenant's own fields under the entity-convention name the generated SystemAdmin tenant-edit screen resolves; tenant:write:update is the hand-written equivalent.",
133
+ // Duplicate of tenant:write:update with weaker logic.
134
+ agent: { expose: false },
133
135
  }));
134
136
  r.screen(tenantListScreen);
135
137
  r.screen(tenantEditScreen);
@@ -10,19 +10,14 @@
10
10
  // invitation = no-op + 200. Cancelling a non-existent invitation
11
11
  // = invitation_not_found.
12
12
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
13
- import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
14
13
  import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
15
14
  import { InternalError, NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
16
15
  import * as z from "zod";
17
- // kumiko-lint-ignore cross-feature-import cancel needs invite-token-store for Redis cleanup
18
- import { invalidateExistingInviteToken } from "../../auth-email-password/invite-token-store.js";
19
- import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../invitation-table.js";
16
+ import { cancelPendingInvitation } from "../cancel-pending-invitation.js";
17
+ import { INVITATION_STATUS, tenantInvitationsTable } from "../invitation-table.js";
20
18
  const CancelInvitationSchema = z.object({
21
19
  invitationId: z.string(),
22
20
  });
23
- const executor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, {
24
- entityName: "tenant-invitation",
25
- });
26
21
  export const cancelInvitationWrite = defineWriteHandler({
27
22
  name: "cancel-invitation",
28
23
  schema: CancelInvitationSchema,
@@ -51,20 +46,10 @@ export const cancelInvitationWrite = defineWriteHandler({
51
46
  if (invitation["status"] !== INVITATION_STATUS.pending) {
52
47
  return { isSuccess: true, data: { id: event.payload.invitationId, alreadyDone: true } };
53
48
  }
54
- // Status update via event-store
55
- const updateResult = await executor.update({
56
- id: event.payload.invitationId,
57
- version: invitation["version"], // @cast-boundary db-row
58
- changes: { status: INVITATION_STATUS.cancelled },
59
- }, event.user, db);
60
- if (!updateResult.isSuccess)
61
- return updateResult;
62
- // Delete the token from Redis (if still there). If Redis is
63
- // unavailable or the token already expired: not a problem, the DB
64
- // row is the single source of truth for the UI.
65
- if (ctx.redis) {
66
- await invalidateExistingInviteToken(ctx.redis, event.payload.invitationId);
67
- }
49
+ const cancelFailure = await cancelPendingInvitation(db, { id: event.payload.invitationId, version: invitation["version"] }, // @cast-boundary db-row
50
+ event.user, ctx.redis);
51
+ if (cancelFailure)
52
+ return cancelFailure;
68
53
  return { isSuccess: true, data: { id: event.payload.invitationId, alreadyDone: false } };
69
54
  },
70
55
  });
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  export declare const memberDirectoryQuery: import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"memberDirectory", z.ZodObject<{
3
3
  limit: z.ZodDefault<z.ZodNumber>;
4
+ search: z.ZodOptional<z.ZodString>;
4
5
  }, z.core.$strip>, {
5
6
  id: string;
6
7
  label: string;
@@ -9,6 +9,10 @@ import { isSystemAdmin } from "./is-system-admin.js";
9
9
  // Shares the KMS adapter's small dedicated pool, same rationale as
10
10
  // members.query.ts.
11
11
  const KMS_POOL_CONCURRENCY = 4;
12
+ // Display names are encrypted PII, so a search cannot run in SQL: candidates
13
+ // are decrypted and filtered in memory. The cap keeps a SystemAdmin's global
14
+ // search from decrypting every user.
15
+ const SEARCH_SCAN_CAP = 1000;
12
16
  async function loadMemberUserIds(db, tenantId) {
13
17
  const memberships = await selectMany(db, tenantMembershipsTable, { tenantId });
14
18
  return [...new Set(memberships.map((row) => String(row["userId"])))];
@@ -29,6 +33,8 @@ export const memberDirectoryQuery = definePagedQueryHandler({
29
33
  name: "memberDirectory",
30
34
  schema: z.object({
31
35
  limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(MAX_LIST_LIMIT),
36
+ // Sent by the reference combobox while the user types.
37
+ search: z.string().trim().min(1).optional(),
32
38
  }),
33
39
  access: { roles: access.admin },
34
40
  description: "Lists users as id/label pairs for resolving user references to display names: the caller's own tenant members for an admin, every user for a SystemAdmin; carries no email or role data.",
@@ -39,24 +45,31 @@ export const memberDirectoryQuery = definePagedQueryHandler({
39
45
  message: "tenant:query:member-directory requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
40
46
  });
41
47
  }
42
- const { limit } = query.payload;
48
+ const { limit, search } = query.payload;
49
+ // Without a search the first `limit` rows are enough; with one, every
50
+ // candidate must be decrypted before the limit can apply.
51
+ const scanLimit = search === undefined ? limit : SEARCH_SCAN_CAP;
43
52
  let users;
44
53
  if (isSystemAdmin(query.user)) {
45
54
  const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin reference labels span every tenant, as user:query:user:list did");
46
- users = await selectMany(db, userTable, undefined, { limit });
55
+ users = await selectMany(db, userTable, undefined, { limit: scanLimit });
47
56
  }
48
57
  else {
49
58
  const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
50
- const userIds = (await loadMemberUserIds(db, query.user.tenantId)).slice(0, limit);
59
+ const userIds = (await loadMemberUserIds(db, query.user.tenantId)).slice(0, scanLimit);
51
60
  users = userIds.length > 0 ? await selectMany(db, userTable, { id: [...userIds] }) : [];
52
61
  }
53
- const rows = await mapWithConcurrency(users, KMS_POOL_CONCURRENCY, async (user) => {
62
+ const resolved = await mapWithConcurrency(users, KMS_POOL_CONCURRENCY, async (user) => {
54
63
  const id = String(user.id);
55
64
  const label = typeof user.displayName === "string"
56
65
  ? await decryptStoredPii(user.displayName, "displayName", "tenant:member-directory")
57
66
  : null;
58
67
  return { id, label: label ?? id };
59
68
  });
69
+ const needle = search?.toLowerCase();
70
+ const rows = needle === undefined
71
+ ? resolved
72
+ : resolved.filter((row) => row.label.toLowerCase().includes(needle)).slice(0, limit);
60
73
  return { rows, nextCursor: null };
61
74
  },
62
75
  });
@@ -4,17 +4,15 @@ import { createSystemUser, defineWriteHandler, withResponseData, } from "@cosmic
4
4
  import { InternalError, NotFoundError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
5
5
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
6
6
  import * as z from "zod";
7
- // kumiko-lint-ignore cross-feature-import cancel needs invite-token-store for Redis cleanup
8
- import { invalidateExistingInviteToken } from "../../auth-email-password/invite-token-store.js";
9
7
  import { decryptStoredPii } from "../../shared/index.js";
10
8
  import { userTable } from "../../user/index.js";
11
- import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../invitation-table.js";
9
+ import { cancelPendingInvitation } from "../cancel-pending-invitation.js";
10
+ import { INVITATION_STATUS, tenantInvitationsTable } from "../invitation-table.js";
12
11
  import { assertNotLastTenantAdmin } from "../last-tenant-admin.js";
13
12
  import { tenantMembershipEntity, tenantMembershipsTable } from "../membership-table.js";
14
13
  const executor = createEventStoreExecutor(tenantMembershipsTable, tenantMembershipEntity, {
15
14
  entityName: "tenant-membership",
16
15
  });
17
- const invitationExecutor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, { entityName: "tenant-invitation" });
18
16
  async function cancelPendingInvitationsOfUser(db, options) {
19
17
  const user = await fetchOne(db, userTable, { id: options.userId });
20
18
  if (!user?.email)
@@ -22,15 +20,9 @@ async function cancelPendingInvitationsOfUser(db, options) {
22
20
  const email = (await decryptStoredPii(user.email, "email", "tenant:remove-member")).toLowerCase();
23
21
  const pendingInvitations = await selectMany(db, tenantInvitationsTable, { tenantId: options.tenantId, email, status: INVITATION_STATUS.pending });
24
22
  for (const invitation of pendingInvitations) {
25
- const updateResult = await invitationExecutor.update({
26
- id: invitation.id,
27
- version: invitation.version,
28
- changes: { status: INVITATION_STATUS.cancelled },
29
- }, options.actor, db);
30
- if (!updateResult.isSuccess)
31
- return updateResult;
32
- if (options.redis)
33
- await invalidateExistingInviteToken(options.redis, invitation.id);
23
+ const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis);
24
+ if (failure)
25
+ return failure;
34
26
  }
35
27
  return undefined;
36
28
  }
@@ -63,7 +55,10 @@ export const removeMemberWrite = defineWriteHandler({
63
55
  }
64
56
  const currentRoles = parseRoles(existing["roles"]);
65
57
  if (currentRoles.includes("TenantAdmin")) {
66
- const lastAdmin = await assertNotLastTenantAdmin(db, event.payload.tenantId, event.payload.userId);
58
+ // assertNotLastTenantAdmin issues raw SQL, so it needs the raw runner, not
59
+ // the tenant-scoped `db` — the count is filtered by the explicit tenantId.
60
+ const lockRunner = ctx.systemDb.unsafeRaw("tenant:write:removeMember last-TenantAdmin advisory lock + membership count");
61
+ const lastAdmin = await assertNotLastTenantAdmin(lockRunner, event.payload.tenantId, event.payload.userId, "remove");
67
62
  if (lastAdmin !== undefined)
68
63
  return lastAdmin;
69
64
  }
@@ -130,7 +130,7 @@ export const teamListQuery = definePagedQueryHandler({
130
130
  const memberRows = membershipRows.map((row) => {
131
131
  const userId = String(row["userId"]);
132
132
  const decrypted = decryptedByUserId.get(userId);
133
- const createdAt = row["createdAt"];
133
+ const createdAt = row["insertedAt"];
134
134
  return {
135
135
  id: String(row["id"]),
136
136
  userId,
@@ -153,7 +153,7 @@ export const teamListQuery = definePagedQueryHandler({
153
153
  const decryptedEmail = typeof email === "string"
154
154
  ? await decryptStoredPii(email, "email", "tenant:team-list")
155
155
  : null;
156
- const createdAt = row["createdAt"];
156
+ const createdAt = row["insertedAt"];
157
157
  const expiresAt = row["expiresAt"];
158
158
  return {
159
159
  id: String(row["id"]),
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  export declare const tenantDirectoryQuery: import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"tenantDirectory", z.ZodObject<{
3
3
  limit: z.ZodDefault<z.ZodNumber>;
4
+ search: z.ZodOptional<z.ZodString>;
4
5
  }, z.core.$strip>, {
5
6
  id: string;
6
7
  label: string;
@@ -11,10 +11,15 @@ import { isSystemAdmin } from "./is-system-admin.js";
11
11
  // them; a TenantAdmin only ever sees their own tenant.
12
12
  // ponytail: capped at `limit` (200 list lookup, 50 combobox) like every
13
13
  // reference lookup; beyond that the cell falls back to the raw id.
14
+ // A search filters in memory over a bounded scan so the combobox can reach
15
+ // tenants beyond the first `limit`; the cap keeps it from reading every tenant.
16
+ const SEARCH_SCAN_CAP = 1000;
14
17
  export const tenantDirectoryQuery = definePagedQueryHandler({
15
18
  name: "tenantDirectory",
16
19
  schema: z.object({
17
20
  limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(MAX_LIST_LIMIT),
21
+ // Sent by the reference combobox while the user types.
22
+ search: z.string().trim().min(1).optional(),
18
23
  }),
19
24
  access: { roles: access.admin },
20
25
  description: "Lists tenants as id/label pairs for resolving tenant references to display names: the caller's own tenant for an admin, every tenant for a SystemAdmin.",
@@ -25,21 +30,26 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
25
30
  message: "tenant:query:tenant-directory requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
26
31
  });
27
32
  }
28
- const { limit } = query.payload;
33
+ const { limit, search } = query.payload;
34
+ const scanLimit = search === undefined ? limit : SEARCH_SCAN_CAP;
29
35
  let tenants;
30
36
  if (isSystemAdmin(query.user)) {
31
37
  const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin reference labels span every tenant, as tenant:query:tenant:list did");
32
- tenants = await selectMany(db, tenantTable, undefined, { limit });
38
+ tenants = await selectMany(db, tenantTable, undefined, { limit: scanLimit });
33
39
  }
34
40
  else {
35
41
  const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
36
42
  const row = await fetchOne(db, tenantTable, { id: query.user.tenantId });
37
43
  tenants = row ? [row] : [];
38
44
  }
39
- const rows = tenants.map((tenant) => {
45
+ const labeled = tenants.map((tenant) => {
40
46
  const id = String(tenant.id);
41
47
  return { id, label: typeof tenant.name === "string" ? tenant.name : id };
42
48
  });
49
+ const needle = search?.toLowerCase();
50
+ const rows = needle === undefined
51
+ ? labeled
52
+ : labeled.filter((row) => row.label.toLowerCase().includes(needle)).slice(0, limit);
43
53
  return { rows, nextCursor: null };
44
54
  },
45
55
  });
@@ -5,6 +5,6 @@ export declare const updateMemberRolesWrite: import("@cosmicdrift/kumiko-types/d
5
5
  roles: z.ZodArray<z.ZodString>;
6
6
  }, z.core.$strip>, {
7
7
  userId: string;
8
- roles: string[];
8
+ roles: readonly string[];
9
9
  tenantId: string;
10
10
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -1,6 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
- import { access, assignableAppRolesFromUsages, createSystemUser, defineWriteHandler, EXT_ASSIGNABLE_ROLE, findForbiddenRoleAssignment, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { access, assignableAppRolesOf, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, mergeAssignedRoles, SYSTEM_ROLE, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { InternalError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
5
5
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
6
6
  import * as z from "zod";
@@ -19,6 +19,15 @@ const REVOKE_ALL_SESSIONS_QN = "sessions:write:user-session:revoke-all-for-user"
19
19
  function resolveTargetTenantId(isSystem, payloadTenantId, sessionTenantId) {
20
20
  return isSystem ? (payloadTenantId ?? sessionTenantId) : sessionTenantId;
21
21
  }
22
+ // Editing your OWN roles keeps the current session: roles are re-derived per
23
+ // request, so the new roles apply at once without a re-login. Other sessions
24
+ // of that user and any foreign edit still revoke all.
25
+ function revokeSessionsPayload(targetUserId, targetTenantId, actor) {
26
+ const payload = { userId: targetUserId, tenantId: targetTenantId };
27
+ if (targetUserId !== actor.id || actor.sid === undefined)
28
+ return payload;
29
+ return { ...payload, exceptSessionId: actor.sid };
30
+ }
22
31
  export const updateMemberRolesWrite = defineWriteHandler({
23
32
  name: "updateMemberRoles",
24
33
  schema: z.object({
@@ -31,14 +40,16 @@ export const updateMemberRolesWrite = defineWriteHandler({
31
40
  // manage memberships cross-tenant (payload.tenantId). TenantAdmin and Admin
32
41
  // are session-scoped (event.user.tenantId).
33
42
  access: { roles: ["system", ...access.admin] },
34
- description: "Replaces the roles a user holds in a tenant, rejecting reserved role names and any grant that would raise the target above the caller; admins act on their own tenant, SystemAdmins on any.",
43
+ description: "Sets the roles a user holds in a tenant, rejecting reserved role names and any grant that would raise the target above the caller; roles the caller could not grant (platform roles such as DataProtectionOfficer or TenantOwner, undeclared or higher-tier app roles) are kept, only the system user replaces the full list; admins act on their own tenant, SystemAdmins on any.",
35
44
  handler: async (event, ctx) => {
36
45
  if (!ctx.systemDb) {
37
46
  throw new InternalError({
38
47
  message: "tenant:write:updateMemberRoles requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
39
48
  });
40
49
  }
41
- const isSystem = event.user.roles.includes("SystemAdmin") || event.user.roles.includes("system");
50
+ const isSystemActor = event.user.roles.includes(SYSTEM_ROLE);
51
+ const isSystem = event.user.roles.includes("SystemAdmin") || isSystemActor;
52
+ const assignableAppRoles = assignableAppRolesOf(ctx.registry);
42
53
  const targetTenantId = resolveTargetTenantId(isSystem, event.payload.tenantId, event.user.tenantId);
43
54
  if (!targetTenantId) {
44
55
  return writeFailure(new ValidationError({
@@ -69,14 +80,18 @@ export const updateMemberRolesWrite = defineWriteHandler({
69
80
  }
70
81
  const row = existing; // @cast-boundary generic-record
71
82
  const currentTargetRoles = parseRoles(row["roles"]);
72
- if (!event.user.roles.includes("system")) {
73
- const forbiddenElevation = findForbiddenRoleAssignment(event.user.roles, event.payload.roles, currentTargetRoles, assignableAppRolesFromUsages(ctx.registry.getExtensionUsages(EXT_ASSIGNABLE_ROLE)));
83
+ if (!isSystemActor) {
84
+ const forbiddenElevation = findForbiddenRoleAssignment(event.user.roles, event.payload.roles, currentTargetRoles, assignableAppRoles);
74
85
  if (forbiddenElevation !== undefined) {
75
86
  return writeFailure(unassignableMembershipRoleError(forbiddenElevation));
76
87
  }
77
88
  }
89
+ // The system user is programmatic and replaces; human actors keep what they cannot grant.
90
+ const nextRoles = isSystemActor
91
+ ? event.payload.roles
92
+ : mergeAssignedRoles(event.user.roles, event.payload.roles, currentTargetRoles, assignableAppRoles);
78
93
  const targetIsTenantAdmin = currentTargetRoles.includes("TenantAdmin");
79
- const willBeTenantAdmin = event.payload.roles.includes("TenantAdmin");
94
+ const willBeTenantAdmin = nextRoles.includes("TenantAdmin");
80
95
  if (targetIsTenantAdmin && !willBeTenantAdmin) {
81
96
  // assertNotLastTenantAdmin issues raw SQL, so it needs the raw runner, not
82
97
  // `db` — the count is already filtered by the explicit targetTenantId.
@@ -99,10 +114,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
99
114
  // requires, see above).
100
115
  const revoker = ctx.registry.getWriteHandler(REVOKE_ALL_SESSIONS_QN);
101
116
  if (revoker) {
102
- await ctx.writeAs(createSystemUser(targetTenantId), REVOKE_ALL_SESSIONS_QN, {
103
- userId: event.payload.userId,
104
- tenantId: targetTenantId,
105
- });
117
+ await ctx.writeAs(createSystemUser(targetTenantId), REVOKE_ALL_SESSIONS_QN, revokeSessionsPayload(event.payload.userId, targetTenantId, event.user));
106
118
  }
107
119
  // Stream tenant follows the actor via streamTenantFor — use the
108
120
  // membership tenant so SystemAdmin/cross-tenant and TenantAdmin paths
@@ -112,8 +124,12 @@ export const updateMemberRolesWrite = defineWriteHandler({
112
124
  const result = await executor.update({
113
125
  id: row["id"], // @cast-boundary db-row
114
126
  version: row["version"], // @cast-boundary db-row
115
- changes: { roles: JSON.stringify(event.payload.roles) },
127
+ changes: { roles: JSON.stringify(nextRoles) },
116
128
  }, { ...event.user, tenantId: targetTenantId }, db);
117
- return withResponseData(result, { ...event.payload, tenantId: targetTenantId });
129
+ return withResponseData(result, {
130
+ ...event.payload,
131
+ roles: nextRoles,
132
+ tenantId: targetTenantId,
133
+ });
118
134
  },
119
135
  });
@@ -34,6 +34,10 @@ export const TENANT_I18N = {
34
34
  "tenant:entity:__action-form__:field:roles:option:Member": { en: "Member" },
35
35
  "tenant:entity:__action-form__:field:roles:option:Admin": { en: "Admin" },
36
36
  "tenant:entity:__action-form__:field:roles:option:TenantAdmin": { en: "Tenant Admin" },
37
+ "tenant:entity:__action-form__:field:roles:option:TenantOwner": { en: "Tenant Owner" },
38
+ "tenant:entity:__action-form__:field:roles:option:DataProtectionOfficer": {
39
+ en: "Data Protection Officer",
40
+ },
37
41
  "tenant:entity:__action-form__:field:roles:option:Editor": { en: "Editor" },
38
42
  "tenant:entity:tenant:field:key": { en: "Key" },
39
43
  "tenant:entity:tenant:field:name": { en: "Name" },
@@ -1,5 +1,5 @@
1
1
  export { TenantCommandSchemas } from "./command-schemas.js";
2
- export { TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries } from "./constants.js";
2
+ export { OWNER_INVITE_ROLE_OPTIONS, TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries, } from "./constants.js";
3
3
  export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
4
4
  export type { InvitationStatus } from "./invitation-table.js";
5
5
  export { INVITATION_STATUS, INVITATION_STATUSES, tenantInvitationEntity, tenantInvitationsTable, } from "./invitation-table.js";
@@ -1,5 +1,5 @@
1
1
  export { TenantCommandSchemas } from "./command-schemas.js";
2
- export { TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries } from "./constants.js";
2
+ export { OWNER_INVITE_ROLE_OPTIONS, TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries, } from "./constants.js";
3
3
  export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
4
4
  export { INVITATION_STATUS, INVITATION_STATUSES, tenantInvitationEntity, tenantInvitationsTable, } from "./invitation-table.js";
5
5
  export { isTenantServingPublicContent } from "./is-tenant-serving-public-content.js";
@@ -17,6 +17,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
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 const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
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;
@@ -65,7 +67,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
65
67
  readonly pending?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
66
68
  } | undefined;
67
69
  readonly optionsQuery?: string;
68
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
70
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
69
71
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
70
72
  required: true;
71
73
  };
@@ -79,6 +81,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
79
81
  readonly filterable?: boolean;
80
82
  readonly encrypted?: boolean;
81
83
  readonly sensitive?: boolean;
84
+ readonly writeOnly?: boolean;
82
85
  readonly readAsInstruction?: boolean;
83
86
  readonly format?: "email" | "url" | "phone" | "password";
84
87
  readonly default?: string;
@@ -116,6 +119,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
116
119
  readonly filterable?: boolean;
117
120
  readonly encrypted?: boolean;
118
121
  readonly sensitive?: boolean;
122
+ readonly writeOnly?: boolean;
119
123
  readonly readAsInstruction?: boolean;
120
124
  readonly format?: "email" | "url" | "phone" | "password";
121
125
  readonly default?: string;
@@ -136,6 +140,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
136
140
  readonly filterable?: boolean;
137
141
  readonly encrypted?: boolean;
138
142
  readonly sensitive?: boolean;
143
+ readonly writeOnly?: boolean;
139
144
  readonly readAsInstruction?: boolean;
140
145
  readonly format?: "email" | "url" | "phone" | "password";
141
146
  readonly default?: string;
@@ -164,7 +169,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
164
169
  readonly pending?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
165
170
  } | undefined;
166
171
  readonly optionsQuery?: string;
167
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
172
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
168
173
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
169
174
  required: true;
170
175
  };
@@ -178,6 +183,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
178
183
  readonly filterable?: boolean;
179
184
  readonly encrypted?: boolean;
180
185
  readonly sensitive?: boolean;
186
+ readonly writeOnly?: boolean;
181
187
  readonly readAsInstruction?: boolean;
182
188
  readonly format?: "email" | "url" | "phone" | "password";
183
189
  readonly default?: string;