@cosmicdrift/kumiko-bundled-features 0.334.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/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/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/handlers/switch-plan.write.js +5 -8
- package/dist/cap-counter/book-cap-usage.js +15 -5
- package/dist/cap-counter/enforce-cap.js +9 -3
- 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/config/db/queries/resolver.js +3 -1
- package/dist/config/handlers/set.write.js +8 -4
- package/dist/config/web/i18n.js +1 -0
- 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/data-retention/resolve-for-tenant.d.ts +2 -3
- package/dist/data-retention/run-retention-cleanup.js +100 -44
- 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/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/files-tenant-data/index.js +3 -2
- package/dist/folders/handlers/clear-folder.write.js +4 -11
- package/dist/folders/handlers/set-folder.write.js +4 -10
- 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/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/tenant-job-failure-table.js +2 -6
- 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/metrics/compute.js +7 -1
- package/dist/metrics/window.js +5 -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.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/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.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/plugin-methods.js +25 -6
- package/dist/subscription-stripe/verify-webhook.js +13 -6
- 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 +8 -5
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/user-content-table.d.ts +2 -5
- 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/last-tenant-admin.d.ts +1 -1
- package/dist/tenant/last-tenant-admin.js +5 -3
- 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/tier-engine/feature.js +3 -2
- 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-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 +2 -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
|
@@ -64,6 +64,13 @@ export async function seedSystemTemplate(db, opts) {
|
|
|
64
64
|
parentTemplateId,
|
|
65
65
|
status: "active",
|
|
66
66
|
};
|
|
67
|
+
const applyUpdate = async (row) => {
|
|
68
|
+
const result = await executor.update({ id: row.id, version: row.version, changes: rowFields }, by, tdb);
|
|
69
|
+
if (!result.isSuccess) {
|
|
70
|
+
throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
|
|
71
|
+
}
|
|
72
|
+
return { id: String(row.id) };
|
|
73
|
+
};
|
|
67
74
|
return runEventStoreSeed({
|
|
68
75
|
existing,
|
|
69
76
|
ifExists: opts.ifExists,
|
|
@@ -83,11 +90,13 @@ export async function seedSystemTemplate(db, opts) {
|
|
|
83
90
|
if (resolved == null) {
|
|
84
91
|
throw new Error(`seedSystemTemplate create unique_violation but row missing`);
|
|
85
92
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
// Race loser goes through the same skip/update contract as a normal re-seed.
|
|
94
|
+
return runEventStoreSeed({
|
|
95
|
+
existing: resolved,
|
|
96
|
+
ifExists: opts.ifExists,
|
|
97
|
+
create: () => Promise.reject(new Error("seedSystemTemplate: unreachable create")),
|
|
98
|
+
update: applyUpdate,
|
|
99
|
+
});
|
|
91
100
|
}
|
|
92
101
|
throw new Error(`seedSystemTemplate create failed: ${JSON.stringify(result)}`);
|
|
93
102
|
}
|
|
@@ -97,13 +106,7 @@ export async function seedSystemTemplate(db, opts) {
|
|
|
97
106
|
}
|
|
98
107
|
return { id: String(data.id) };
|
|
99
108
|
},
|
|
100
|
-
update:
|
|
101
|
-
const result = await executor.update({ id: row.id, version: row.version, changes: rowFields }, by, tdb);
|
|
102
|
-
if (!result.isSuccess) {
|
|
103
|
-
throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
|
|
104
|
-
}
|
|
105
|
-
return { id: String(row.id) };
|
|
106
|
-
},
|
|
109
|
+
update: applyUpdate,
|
|
107
110
|
});
|
|
108
111
|
}
|
|
109
112
|
// Boot/test seed for kind=text-block. Same projection path as set.write,
|
|
@@ -132,6 +135,24 @@ export async function seedTextBlock(db, opts) {
|
|
|
132
135
|
parentTemplateId: null,
|
|
133
136
|
status: "active",
|
|
134
137
|
};
|
|
138
|
+
const applyUpdate = async (row, current) => {
|
|
139
|
+
// Skip no-op updates so a legal re-seed on every pod boot does not bump
|
|
140
|
+
// version/modifiedAt (ETag, "last changed", event-store growth).
|
|
141
|
+
if (current?.title === fields.title &&
|
|
142
|
+
current?.content === fields.content &&
|
|
143
|
+
current?.folder === fields.folder) {
|
|
144
|
+
return { id: String(row.id) };
|
|
145
|
+
}
|
|
146
|
+
const result = await executor.update({
|
|
147
|
+
id: row.id,
|
|
148
|
+
version: row.version,
|
|
149
|
+
changes: { title: fields.title, content: fields.content, folder: fields.folder },
|
|
150
|
+
}, by, tdb);
|
|
151
|
+
if (!result.isSuccess) {
|
|
152
|
+
throw new Error(`seedTextBlock update failed: ${JSON.stringify(result)}`);
|
|
153
|
+
}
|
|
154
|
+
return { id: String(row.id) };
|
|
155
|
+
};
|
|
135
156
|
return runEventStoreSeed({
|
|
136
157
|
existing,
|
|
137
158
|
ifExists: opts.ifExists,
|
|
@@ -149,15 +170,13 @@ export async function seedTextBlock(db, opts) {
|
|
|
149
170
|
if (resolved == null) {
|
|
150
171
|
throw new Error("seedTextBlock create unique_violation but row missing");
|
|
151
172
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
return { id: resolved.id };
|
|
173
|
+
// Race loser goes through the same skip/no-op/update contract as a normal re-seed.
|
|
174
|
+
return runEventStoreSeed({
|
|
175
|
+
existing: resolved,
|
|
176
|
+
ifExists: opts.ifExists,
|
|
177
|
+
create: () => Promise.reject(new Error("seedTextBlock: unreachable create")),
|
|
178
|
+
update: (row) => applyUpdate(row, again),
|
|
179
|
+
});
|
|
161
180
|
}
|
|
162
181
|
throw new Error(`seedTextBlock create failed: ${JSON.stringify(result)}`);
|
|
163
182
|
}
|
|
@@ -169,24 +188,7 @@ export async function seedTextBlock(db, opts) {
|
|
|
169
188
|
}
|
|
170
189
|
return { id: String(data.id) };
|
|
171
190
|
},
|
|
172
|
-
update:
|
|
173
|
-
// Skip no-op updates so a legal re-seed on every pod boot does not bump
|
|
174
|
-
// version/updatedAt (ETag, "last changed", event-store growth).
|
|
175
|
-
if (existingRow?.title === fields.title &&
|
|
176
|
-
existingRow?.content === fields.content &&
|
|
177
|
-
existingRow?.folder === fields.folder) {
|
|
178
|
-
return { id: String(row.id) };
|
|
179
|
-
}
|
|
180
|
-
const result = await executor.update({
|
|
181
|
-
id: row.id,
|
|
182
|
-
version: row.version,
|
|
183
|
-
changes: { title: fields.title, content: fields.content, folder: fields.folder },
|
|
184
|
-
}, by, tdb);
|
|
185
|
-
if (!result.isSuccess) {
|
|
186
|
-
throw new Error(`seedTextBlock update failed: ${JSON.stringify(result)}`);
|
|
187
|
-
}
|
|
188
|
-
return { id: String(row.id) };
|
|
189
|
-
},
|
|
191
|
+
update: (row) => applyUpdate(row, existingRow),
|
|
190
192
|
});
|
|
191
193
|
}
|
|
192
194
|
// Boot seed for legal/marketing copy from a JSON template. `ifExists: "update"`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Temporal } from "temporal-polyfill";
|
|
1
2
|
export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
|
|
2
3
|
slug: {
|
|
3
4
|
readonly type: "text";
|
|
@@ -482,6 +483,12 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
|
|
|
482
483
|
required: true;
|
|
483
484
|
};
|
|
484
485
|
}, "tenant">>;
|
|
486
|
+
export type EntityAuditColumns = {
|
|
487
|
+
readonly insertedAt: Temporal.Instant;
|
|
488
|
+
readonly modifiedAt: Temporal.Instant | null;
|
|
489
|
+
readonly insertedById: string | null;
|
|
490
|
+
readonly modifiedById: string | null;
|
|
491
|
+
};
|
|
485
492
|
export type TemplateResourceRow = {
|
|
486
493
|
readonly id: string | number;
|
|
487
494
|
readonly version: number;
|
|
@@ -498,8 +505,4 @@ export type TemplateResourceRow = {
|
|
|
498
505
|
readonly scope: string;
|
|
499
506
|
readonly parentTemplateId: string | null;
|
|
500
507
|
readonly status: string;
|
|
501
|
-
|
|
502
|
-
readonly updatedAt: Date;
|
|
503
|
-
readonly createdBy: string;
|
|
504
|
-
readonly updatedBy: string;
|
|
505
|
-
};
|
|
508
|
+
} & EntityAuditColumns;
|
|
@@ -24,8 +24,8 @@ async function seedTemplate(db, args) {
|
|
|
24
24
|
variableSchema: JSON.stringify(args.variableSchema ?? {}),
|
|
25
25
|
linkedResources: JSON.stringify(args.linkedResources ?? {}),
|
|
26
26
|
parentTemplateId: args.parentTemplateId ?? null,
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
insertedById: "conformance",
|
|
28
|
+
modifiedById: "conformance",
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
export async function assertConsumerHandlesNotFound(consumer, opts) {
|
|
@@ -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";
|
|
@@ -348,8 +349,4 @@ export type UserContentEntryRow = {
|
|
|
348
349
|
readonly folder: string | null;
|
|
349
350
|
readonly content: string | null;
|
|
350
351
|
readonly contentFormat: string;
|
|
351
|
-
|
|
352
|
-
readonly updatedAt: Date;
|
|
353
|
-
readonly createdBy: string;
|
|
354
|
-
readonly updatedBy: string;
|
|
355
|
-
};
|
|
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";
|
|
@@ -3,5 +3,5 @@ import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
|
3
3
|
export declare const LAST_TENANT_ADMIN_LOCK_NAMESPACE = 1952539757;
|
|
4
4
|
type TenantDb = Parameters<typeof acquireNamespacedAdvisoryLock>[0];
|
|
5
5
|
/** Refuse demoting/removing the last TenantAdmin for `tenantId`. */
|
|
6
|
-
export declare function assertNotLastTenantAdmin(db: TenantDb, tenantId: string, userId: string): Promise<ReturnType<typeof writeFailure> | undefined>;
|
|
6
|
+
export declare function assertNotLastTenantAdmin(db: TenantDb, tenantId: string, userId: string, action?: "demote" | "remove"): Promise<ReturnType<typeof writeFailure> | undefined>;
|
|
7
7
|
export {};
|