@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.
- package/dist/admin-shell/overview-panels.js +2 -2
- package/dist/agent-tools/agent-doc-lint.d.ts +1 -0
- package/dist/agent-tools/agent-doc-lint.js +33 -3
- package/dist/agent-tools/agent-manifest.js +47 -6
- package/dist/agent-tools/filter-ops.d.ts +2 -0
- package/dist/agent-tools/filter-ops.js +4 -0
- package/dist/agent-tools/tool-catalog.js +1 -4
- package/dist/agent-tools/tool-dispatch.js +1 -4
- package/dist/audit/constants.d.ts +1 -1
- package/dist/audit/constants.js +1 -1
- package/dist/audit/handlers/list.query.js +4 -3
- package/dist/auth-email-password/handlers/change-password.write.js +2 -0
- package/dist/auth-email-password/handlers/invite-create.write.js +12 -2
- package/dist/auth-email-password/handlers/login.write.js +2 -0
- package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
- package/dist/auth-email-password/i18n.js +2 -0
- package/dist/auth-email-password/web/auth-client.d.ts +1 -0
- package/dist/auth-email-password/web/auth-client.js +1 -1
- package/dist/auth-email-password/web/auth-gate.js +6 -8
- package/dist/auth-email-password/web/index.d.ts +1 -1
- package/dist/auth-email-password/web/index.js +1 -1
- package/dist/auth-email-password/web/invite-accept-screen.js +16 -12
- package/dist/auth-email-password/web/login-screen.js +1 -1
- package/dist/auth-email-password/web/session-bootstrap-error.d.ts +5 -1
- package/dist/auth-email-password/web/session-bootstrap-error.js +2 -2
- package/dist/auth-email-password/web/session.d.ts +4 -0
- package/dist/auth-email-password/web/session.js +15 -0
- package/dist/auth-mfa/feature.js +3 -2
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +2 -2
- package/dist/auth-mfa/handlers/verify.write.js +2 -0
- package/dist/auth-mfa/hmac-token-codec.js +3 -0
- package/dist/auth-mfa/mfa-challenge-token.js +4 -0
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/web/mfa-client.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-client.js +4 -10
- package/dist/billing-foundation/billing-info-query.js +1 -1
- package/dist/billing-foundation/checkout-core.d.ts +5 -5
- package/dist/billing-foundation/checkout-core.js +10 -6
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/switch-plan.write.js +5 -8
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/book-cap-usage.js +15 -5
- package/dist/cap-counter/enforce-cap.js +9 -3
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-counter/feature.js +3 -2
- package/dist/cap-counter/i18n.d.ts +1 -0
- package/dist/cap-counter/i18n.js +6 -5
- package/dist/cap-overview/handlers/caps-usage.query.js +9 -16
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +41 -15
- package/dist/cap-overview/i18n.js +5 -0
- package/dist/cap-overview/web/cap-cards-panel.js +4 -1
- package/dist/cap-overview/web/cap-usage-bar.js +1 -1
- package/dist/channel-email/email-channel.js +12 -1
- package/dist/channel-in-app/tables.js +2 -6
- package/dist/compliance-profiles/handlers/sub-processors.query.js +1 -1
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
- package/dist/config/db/queries/resolver.js +3 -1
- package/dist/config/handlers/set.write.js +8 -4
- package/dist/config/table.d.ts +6 -0
- package/dist/config/web/i18n.js +13 -4
- package/dist/config/write-helpers.d.ts +1 -1
- package/dist/config/write-helpers.js +11 -5
- package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -8
- package/dist/custom-fields/constants.js +1 -4
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/resolve-for-tenant.d.ts +2 -3
- package/dist/data-retention/run-retention-cleanup.js +100 -44
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/types.d.ts +2 -1
- package/dist/delivery/types.js +7 -0
- package/dist/delivery/unsubscribe.js +5 -11
- package/dist/derivatives-sharp/render.js +27 -9
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/document-ingest-foundation/feature.js +5 -5
- package/dist/document-ingest-foundation/providers.d.ts +1 -0
- package/dist/document-ingest-foundation/providers.js +6 -0
- package/dist/document-ingest-foundation/write-document-extract.d.ts +2 -2
- package/dist/document-ingest-foundation/write-document-extract.js +10 -0
- package/dist/feature-toggles/global-feature-state-table.js +2 -7
- package/dist/feature-toggles/toggle-sync-signal.d.ts +2 -1
- package/dist/feature-toggles/toggle-sync-signal.js +3 -2
- package/dist/file-provider-s3/feature.js +1 -1
- package/dist/files-tenant-data/index.js +3 -2
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/handlers/clear-folder.write.js +4 -11
- package/dist/folders/handlers/set-folder.write.js +4 -10
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/feature.js +3 -2
- package/dist/form-draft/handlers/discard.write.js +7 -8
- package/dist/form-draft/handlers/get.query.js +7 -1
- package/dist/foundation-shared/host-allowlist.d.ts +4 -0
- package/dist/foundation-shared/host-allowlist.js +14 -0
- package/dist/foundation-shared/index.d.ts +1 -0
- package/dist/foundation-shared/index.js +1 -0
- package/dist/foundation-shared/mail-host-policy.d.ts +0 -2
- package/dist/foundation-shared/mail-host-policy.js +3 -13
- package/dist/inbound-mail-foundation/connect-routes.js +7 -5
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +47 -43
- package/dist/jobs/handlers/list.query.d.ts +7 -2
- package/dist/jobs/handlers/list.query.js +9 -2
- package/dist/jobs/handlers/retry.write.js +3 -2
- package/dist/jobs/handlers/tenant-failures.query.js +3 -9
- package/dist/jobs/job-run-logger.js +18 -2
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/jobs/tenant-job-failure-table.js +2 -6
- package/dist/ledger/entity.d.ts +16 -3
- package/dist/ledger/handlers/confirm-schedule-period.write.js +2 -2
- package/dist/ledger/handlers/reverse-transaction.write.js +2 -2
- package/dist/ledger/subject-reference.d.ts +4 -0
- package/dist/ledger/subject-reference.js +7 -0
- package/dist/legal-pages/feature.js +8 -1
- package/dist/managed-pages/handlers/branding.query.js +1 -1
- package/dist/managed-pages/handlers/by-slug.query.js +1 -1
- package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -1
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/metrics/compute.js +7 -1
- package/dist/metrics/window.js +5 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/handlers/add-note.write.js +4 -9
- package/dist/notes-history-user-data/hooks.js +8 -1
- package/dist/personal-access-tokens/constants.d.ts +1 -0
- package/dist/personal-access-tokens/constants.js +1 -0
- package/dist/personal-access-tokens/feature.js +2 -2
- package/dist/personal-access-tokens/handlers/create.write.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +2 -2
- package/dist/personal-access-tokens/i18n.js +12 -13
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/schema/api-token.js +2 -1
- package/dist/personal-access-tokens/screens.js +5 -1
- package/dist/rate-limiting/feature.js +2 -0
- package/dist/rate-limiting/handlers/bucket-access.js +15 -7
- package/dist/rate-limiting/i18n.d.ts +10 -0
- package/dist/rate-limiting/i18n.js +10 -0
- package/dist/secrets/feature.js +3 -2
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/handlers/seo-config.query.js +1 -1
- package/dist/sessions/constants.d.ts +1 -0
- package/dist/sessions/constants.js +1 -0
- package/dist/sessions/feature.js +5 -3
- package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -0
- package/dist/sessions/handlers/revoke-all-for-user.write.js +12 -2
- package/dist/sessions/handlers/revoke.write.js +2 -2
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/schema/user-session.js +2 -1
- package/dist/sessions/screens.js +5 -1
- package/dist/sessions/session-callbacks.js +2 -2
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/lockout-counter.js +1 -1
- package/dist/shared/parent-visibility.d.ts +5 -0
- package/dist/shared/parent-visibility.js +17 -1
- package/dist/shared/row-bound-grant.js +20 -9
- package/dist/shared/single-use-token-store.js +5 -13
- package/dist/step-dispatcher/webhook-runner.d.ts +0 -2
- package/dist/step-dispatcher/webhook-runner.js +18 -17
- package/dist/subscription-mollie/plugin-methods.d.ts +3 -2
- package/dist/subscription-mollie/plugin-methods.js +7 -1
- package/dist/subscription-mollie/verify-webhook.js +6 -0
- package/dist/subscription-stripe/feature.js +3 -3
- package/dist/subscription-stripe/plugin-methods.js +25 -6
- package/dist/subscription-stripe/verify-webhook.js +13 -6
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/handlers/assign-tag.write.js +4 -10
- package/dist/tags/handlers/remove-tag.write.js +4 -11
- package/dist/template-resolver/api.d.ts +2 -1
- package/dist/template-resolver/api.js +1 -1
- package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
- package/dist/template-resolver/handlers/by-slug.query.js +2 -2
- package/dist/template-resolver/handlers/by-tenant.query.d.ts +3 -2
- package/dist/template-resolver/handlers/by-tenant.query.js +2 -2
- package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-shared.d.ts +3 -2
- package/dist/template-resolver/handlers/collection-shared.js +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
- package/dist/template-resolver/handlers/list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/list.query.js +1 -1
- package/dist/template-resolver/seeding.js +41 -39
- package/dist/template-resolver/table.d.ts +26 -13
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/user-content-table.d.ts +16 -9
- package/dist/template-resolver/web/client-plugin.d.ts +1 -1
- package/dist/template-resolver-user-data/hooks.js +1 -1
- package/dist/tenant/cancel-pending-invitation.d.ts +8 -0
- package/dist/tenant/cancel-pending-invitation.js +22 -0
- package/dist/tenant/feature.js +4 -2
- package/dist/tenant/handlers/cancel-invitation.write.js +6 -21
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +17 -4
- package/dist/tenant/handlers/remove-member.write.js +9 -14
- package/dist/tenant/handlers/team-list.query.js +2 -2
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +13 -3
- package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
- package/dist/tenant/handlers/update-member-roles.write.js +28 -12
- package/dist/tenant/i18n.js +4 -0
- package/dist/tenant/index.d.ts +1 -1
- package/dist/tenant/index.js +1 -1
- package/dist/tenant/invitation-table.d.ts +8 -2
- package/dist/tenant/last-tenant-admin.d.ts +1 -1
- package/dist/tenant/last-tenant-admin.js +5 -3
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +8 -2
- package/dist/tenant/seeding.d.ts +4 -0
- package/dist/tenant/seeding.js +9 -2
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +27 -1
- package/dist/tenant-lifecycle/feature.js +2 -1
- package/dist/tenant-lifecycle/stages.js +2 -2
- package/dist/tenant-settings/feature.js +9 -6
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/feature.js +3 -2
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/command-schemas.d.ts +2 -2
- package/dist/user/handlers/create.write.d.ts +1 -1
- package/dist/user/handlers/create.write.js +8 -2
- package/dist/user/handlers/update.write.d.ts +1 -1
- package/dist/user/handlers/update.write.js +8 -2
- package/dist/user/schema/user.d.ts +14 -2
- package/dist/user-data-rights/db/queries/export-jobs.d.ts +2 -0
- package/dist/user-data-rights/feature.js +22 -9
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +4 -7
- package/dist/user-data-rights/handlers/deletion-grace-period.d.ts +3 -2
- package/dist/user-data-rights/handlers/deletion-grace-period.js +30 -8
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +3 -6
- package/dist/user-data-rights/lib/update-user-lifecycle.js +7 -14
- package/dist/user-data-rights/run-export-jobs.d.ts +1 -0
- package/dist/user-data-rights/run-export-jobs.js +74 -41
- package/dist/user-data-rights/run-forget-cleanup.js +2 -2
- package/dist/user-data-rights/run-user-export.js +8 -4
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +8 -2
- package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +4 -1
- package/dist/user-profile/feature.js +6 -1
- package/dist/user-profile/handlers/change-email.write.js +2 -0
- package/dist/workflow-runner/db/queries/due-runs.js +6 -9
- package/dist/workflow-runner/event-subscriber.js +29 -9
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -5
- package/dist/workflow-runner/pending-projection.js +8 -1
- package/package.json +11 -9
- package/src/agent-tools/changes.json +12 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +12 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/data-retention/changes.json +12 -0
- package/src/delivery/changes.json +7 -0
- package/src/derivatives-sharp/changes.json +11 -0
- package/src/document-ingest-foundation/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +12 -0
- package/src/jobs/changes.json +13 -0
- package/src/legal-pages/changes.json +8 -1
- package/src/managed-pages/changes.json +5 -0
- package/src/metrics/changes.json +6 -0
- package/src/rate-limiting/changes.json +5 -0
- package/src/secrets/changes.json +7 -0
- package/src/sessions/changes.json +5 -0
- package/src/shared/changes.json +6 -0
- package/src/step-dispatcher/changes.json +5 -0
- package/src/subscription-mollie/changes.json +7 -0
- package/src/subscription-stripe/changes.json +11 -0
- package/src/template-resolver/changes.json +7 -0
- package/src/tenant/changes.json +29 -0
- package/src/user-data-rights/changes.json +22 -0
- 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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
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
|
|
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?: {
|
|
@@ -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
|
+
}
|
package/dist/tenant/feature.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
import {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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,
|
|
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
|
|
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 {
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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["
|
|
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["
|
|
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
|
|
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,
|
|
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: "
|
|
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
|
|
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 (!
|
|
73
|
-
const forbiddenElevation = findForbiddenRoleAssignment(event.user.roles, event.payload.roles, currentTargetRoles,
|
|
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 =
|
|
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(
|
|
127
|
+
changes: { roles: JSON.stringify(nextRoles) },
|
|
116
128
|
}, { ...event.user, tenantId: targetTenantId }, db);
|
|
117
|
-
return withResponseData(result, {
|
|
129
|
+
return withResponseData(result, {
|
|
130
|
+
...event.payload,
|
|
131
|
+
roles: nextRoles,
|
|
132
|
+
tenantId: targetTenantId,
|
|
133
|
+
});
|
|
118
134
|
},
|
|
119
135
|
});
|
package/dist/tenant/i18n.js
CHANGED
|
@@ -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" },
|
package/dist/tenant/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/tenant/index.js
CHANGED
|
@@ -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?:
|
|
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?:
|
|
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;
|