@cosmicdrift/kumiko-bundled-features 0.355.0 → 0.357.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/auth-mfa/i18n.js +1 -0
  2. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
  3. package/dist/auth-mfa/screens.js +8 -1
  4. package/dist/billing-foundation/entities.d.ts +8 -0
  5. package/dist/billing-foundation/projection.d.ts +8 -0
  6. package/dist/cap-counter/entity.d.ts +1 -0
  7. package/dist/cap-counter/feature.js +7 -1
  8. package/dist/cap-overview/screens.js +3 -1
  9. package/dist/compliance-profiles/handlers/set-profile.write.js +14 -22
  10. package/dist/config/table.d.ts +6 -0
  11. package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -3
  12. package/dist/custom-fields/entity.d.ts +4 -0
  13. package/dist/custom-fields/handlers/define-system-field.write.js +8 -16
  14. package/dist/custom-fields/handlers/delete-system-field.write.js +5 -2
  15. package/dist/custom-fields/lib/define-or-resurrect.d.ts +2 -1
  16. package/dist/custom-fields/lib/define-or-resurrect.js +11 -4
  17. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
  18. package/dist/delivery/chat-webhook-sender.js +1 -1
  19. package/dist/delivery/constants.d.ts +1 -0
  20. package/dist/delivery/constants.js +4 -0
  21. package/dist/delivery/delivery-service.js +62 -25
  22. package/dist/delivery/feature.js +7 -1
  23. package/dist/delivery/jobs.js +25 -1
  24. package/dist/delivery/tables.d.ts +6 -0
  25. package/dist/delivery/testing.d.ts +1 -0
  26. package/dist/document-ingest-foundation/entity.d.ts +4 -0
  27. package/dist/folders/entity.d.ts +8 -0
  28. package/dist/form-draft/entity.d.ts +2 -0
  29. package/dist/inbound-mail-foundation/entities.d.ts +25 -0
  30. package/dist/inbound-mail-foundation/projection.d.ts +20 -0
  31. package/dist/jobs/job-run-table.d.ts +6 -0
  32. package/dist/ledger/entity.d.ts +13 -0
  33. package/dist/managed-pages/handlers/set.write.js +6 -17
  34. package/dist/managed-pages/table.d.ts +12 -0
  35. package/dist/notes-history/entity.d.ts +12 -0
  36. package/dist/notes-history/entity.js +7 -6
  37. package/dist/notes-history-user-data/hooks.d.ts +10 -1
  38. package/dist/notes-history-user-data/hooks.js +78 -61
  39. package/dist/notes-history-user-data/index.d.ts +1 -0
  40. package/dist/notes-history-user-data/index.js +2 -1
  41. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
  42. package/dist/personal-access-tokens/screens.js +2 -0
  43. package/dist/secrets/table.d.ts +1 -0
  44. package/dist/sessions/schema/user-session.d.ts +4 -0
  45. package/dist/sessions/screens.js +2 -0
  46. package/dist/shared/index.d.ts +1 -0
  47. package/dist/shared/index.js +1 -0
  48. package/dist/shared/require-for-tenant.d.ts +2 -0
  49. package/dist/shared/require-for-tenant.js +9 -0
  50. package/dist/tags/entity.d.ts +9 -0
  51. package/dist/template-resolver/handlers/collection-set.write.js +4 -4
  52. package/dist/template-resolver/handlers/set.write.js +6 -7
  53. package/dist/template-resolver/handlers/upsert-system.write.js +6 -6
  54. package/dist/template-resolver/handlers/upsert-tenant.write.js +6 -4
  55. package/dist/template-resolver/table.d.ts +10 -0
  56. package/dist/template-resolver/user-content-table.d.ts +10 -0
  57. package/dist/tenant/cancel-pending-invitation.d.ts +2 -2
  58. package/dist/tenant/cancel-pending-invitation.js +2 -2
  59. package/dist/tenant/constants.d.ts +1 -0
  60. package/dist/tenant/constants.js +6 -3
  61. package/dist/tenant/feature.js +2 -0
  62. package/dist/tenant/handlers/member-role-availability.query.d.ts +7 -0
  63. package/dist/tenant/handlers/member-role-availability.query.js +18 -0
  64. package/dist/tenant/handlers/remove-member.write.js +13 -10
  65. package/dist/tenant/handlers/update-member-roles.write.js +12 -9
  66. package/dist/tenant/invitation-table.d.ts +6 -0
  67. package/dist/tenant/member-role-options.d.ts +2 -0
  68. package/dist/tenant/member-role-options.js +4 -0
  69. package/dist/tenant/membership-table.d.ts +4 -0
  70. package/dist/tenant/schema/tenant.d.ts +6 -0
  71. package/dist/tenant/screens.d.ts +4 -2
  72. package/dist/tenant/screens.js +10 -6
  73. package/dist/tier-engine/entity.d.ts +2 -0
  74. package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -18
  75. package/dist/tier-engine/tier-resolver.d.ts +2 -0
  76. package/dist/user/schema/user.d.ts +12 -0
  77. package/dist/user/screens.js +8 -1
  78. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
  79. package/dist/user-data-rights/schema/download-token.d.ts +8 -0
  80. package/dist/user-data-rights/schema/export-job.d.ts +6 -0
  81. package/dist/user-data-rights/screens.js +8 -1
  82. package/dist/waitlist/entity.d.ts +14 -0
  83. package/dist/waitlist/screens.js +8 -1
  84. package/package.json +9 -9
  85. package/src/auth-mfa/changes.json +6 -0
  86. package/src/crypto-shredding/changes.json +6 -0
  87. package/src/delivery/changes.json +6 -0
  88. package/src/tenant/changes.json +6 -0
@@ -5,6 +5,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
5
5
  readonly description?: string;
6
6
  readonly maxLength?: number;
7
7
  readonly minLength?: number;
8
+ readonly autoComplete?: string;
8
9
  readonly required?: boolean;
9
10
  readonly searchable?: boolean;
10
11
  readonly sortable?: boolean;
@@ -53,6 +54,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
53
54
  readonly description?: string;
54
55
  readonly maxLength?: number;
55
56
  readonly minLength?: number;
57
+ readonly autoComplete?: string;
56
58
  readonly required?: boolean;
57
59
  readonly searchable?: boolean;
58
60
  readonly sortable?: boolean;
@@ -75,6 +77,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
75
77
  readonly description?: string;
76
78
  readonly maxLength?: number;
77
79
  readonly minLength?: number;
80
+ readonly autoComplete?: string;
78
81
  readonly required?: boolean;
79
82
  readonly searchable?: boolean;
80
83
  readonly sortable?: boolean;
@@ -97,6 +100,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
97
100
  readonly description?: string;
98
101
  readonly maxLength?: number;
99
102
  readonly minLength?: number;
103
+ readonly autoComplete?: string;
100
104
  readonly required?: boolean;
101
105
  readonly searchable?: boolean;
102
106
  readonly sortable?: boolean;
@@ -213,6 +217,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
213
217
  readonly description?: string;
214
218
  readonly maxLength?: number;
215
219
  readonly minLength?: number;
220
+ readonly autoComplete?: string;
216
221
  readonly required?: boolean;
217
222
  readonly searchable?: boolean;
218
223
  readonly sortable?: boolean;
@@ -260,6 +265,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
260
265
  readonly description?: string;
261
266
  readonly maxLength?: number;
262
267
  readonly minLength?: number;
268
+ readonly autoComplete?: string;
263
269
  readonly required?: boolean;
264
270
  readonly searchable?: boolean;
265
271
  readonly sortable?: boolean;
@@ -308,6 +314,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
308
314
  readonly description?: string;
309
315
  readonly maxLength?: number;
310
316
  readonly minLength?: number;
317
+ readonly autoComplete?: string;
311
318
  readonly required?: boolean;
312
319
  readonly searchable?: boolean;
313
320
  readonly sortable?: boolean;
@@ -330,6 +337,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
330
337
  readonly description?: string;
331
338
  readonly maxLength?: number;
332
339
  readonly minLength?: number;
340
+ readonly autoComplete?: string;
333
341
  readonly required?: boolean;
334
342
  readonly searchable?: boolean;
335
343
  readonly sortable?: boolean;
@@ -352,6 +360,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
352
360
  readonly description?: string;
353
361
  readonly maxLength?: number;
354
362
  readonly minLength?: number;
363
+ readonly autoComplete?: string;
355
364
  readonly required?: boolean;
356
365
  readonly searchable?: boolean;
357
366
  readonly sortable?: boolean;
@@ -468,6 +477,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
468
477
  readonly description?: string;
469
478
  readonly maxLength?: number;
470
479
  readonly minLength?: number;
480
+ readonly autoComplete?: string;
471
481
  readonly required?: boolean;
472
482
  readonly searchable?: boolean;
473
483
  readonly sortable?: boolean;
@@ -5,6 +5,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
5
5
  readonly description?: string;
6
6
  readonly maxLength?: number;
7
7
  readonly minLength?: number;
8
+ readonly autoComplete?: string;
8
9
  readonly required?: boolean;
9
10
  readonly searchable?: boolean;
10
11
  readonly sortable?: boolean;
@@ -27,6 +28,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
27
28
  readonly description?: string;
28
29
  readonly maxLength?: number;
29
30
  readonly minLength?: number;
31
+ readonly autoComplete?: string;
30
32
  readonly required?: boolean;
31
33
  readonly searchable?: boolean;
32
34
  readonly sortable?: boolean;
@@ -75,6 +77,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
75
77
  readonly description?: string;
76
78
  readonly maxLength?: number;
77
79
  readonly minLength?: number;
80
+ readonly autoComplete?: string;
78
81
  readonly required?: boolean;
79
82
  readonly searchable?: boolean;
80
83
  readonly sortable?: boolean;
@@ -97,6 +100,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
97
100
  readonly description?: string;
98
101
  readonly maxLength?: number;
99
102
  readonly minLength?: number;
103
+ readonly autoComplete?: string;
100
104
  readonly required?: boolean;
101
105
  readonly searchable?: boolean;
102
106
  readonly sortable?: boolean;
@@ -119,6 +123,7 @@ export declare const userContentEntryEntity: import("@cosmicdrift/kumiko-types/f
119
123
  readonly description?: string;
120
124
  readonly maxLength?: number;
121
125
  readonly minLength?: number;
126
+ readonly autoComplete?: string;
122
127
  readonly required?: boolean;
123
128
  readonly searchable?: boolean;
124
129
  readonly sortable?: boolean;
@@ -183,6 +188,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
183
188
  readonly description?: string;
184
189
  readonly maxLength?: number;
185
190
  readonly minLength?: number;
191
+ readonly autoComplete?: string;
186
192
  readonly required?: boolean;
187
193
  readonly searchable?: boolean;
188
194
  readonly sortable?: boolean;
@@ -205,6 +211,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
205
211
  readonly description?: string;
206
212
  readonly maxLength?: number;
207
213
  readonly minLength?: number;
214
+ readonly autoComplete?: string;
208
215
  readonly required?: boolean;
209
216
  readonly searchable?: boolean;
210
217
  readonly sortable?: boolean;
@@ -253,6 +260,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
253
260
  readonly description?: string;
254
261
  readonly maxLength?: number;
255
262
  readonly minLength?: number;
263
+ readonly autoComplete?: string;
256
264
  readonly required?: boolean;
257
265
  readonly searchable?: boolean;
258
266
  readonly sortable?: boolean;
@@ -275,6 +283,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
275
283
  readonly description?: string;
276
284
  readonly maxLength?: number;
277
285
  readonly minLength?: number;
286
+ readonly autoComplete?: string;
278
287
  readonly required?: boolean;
279
288
  readonly searchable?: boolean;
280
289
  readonly sortable?: boolean;
@@ -297,6 +306,7 @@ export declare const userContentEntriesTable: import("@cosmicdrift/kumiko-framew
297
306
  readonly description?: string;
298
307
  readonly maxLength?: number;
299
308
  readonly minLength?: number;
309
+ readonly autoComplete?: string;
300
310
  readonly required?: boolean;
301
311
  readonly searchable?: boolean;
302
312
  readonly sortable?: boolean;
@@ -1,8 +1,8 @@
1
1
  import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
- import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import type { Redis } from "ioredis";
5
5
  export declare function cancelPendingInvitation(db: TenantDb, invitation: {
6
6
  readonly id: string;
7
7
  readonly version: number;
8
- }, actor: SessionUser, redis: Redis | undefined): Promise<WriteFailure | undefined>;
8
+ }, actor: SessionUser, redis: Redis | undefined, streamTenantId?: TenantId): Promise<WriteFailure | undefined>;
@@ -5,12 +5,12 @@ import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } fr
5
5
  const invitationExecutor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, { entityName: "tenant-invitation" });
6
6
  // Shared by cancel-invitation and remove-member so the cancel behavior
7
7
  // (status transition + token invalidation) cannot drift between them.
8
- export async function cancelPendingInvitation(db, invitation, actor, redis) {
8
+ export async function cancelPendingInvitation(db, invitation, actor, redis, streamTenantId) {
9
9
  const updateResult = await invitationExecutor.update({
10
10
  id: invitation.id,
11
11
  version: invitation.version,
12
12
  changes: { status: INVITATION_STATUS.cancelled },
13
- }, actor, db);
13
+ }, actor, db, { streamTenantId });
14
14
  if (!updateResult.isSuccess)
15
15
  return updateResult;
16
16
  // Delete the token from Redis (if still there). If Redis is
@@ -33,6 +33,7 @@ export declare const TenantQueries: {
33
33
  readonly resolveUserIds: "tenant:query:resolve-user-ids";
34
34
  readonly invitations: "tenant:query:invitations";
35
35
  readonly teamList: "tenant:query:team:list";
36
+ readonly memberRoleAvailability: "tenant:query:member-role-availability";
36
37
  };
37
38
  export declare const TenantErrors: {
38
39
  readonly membershipNotFound: "membership_not_found";
@@ -19,9 +19,10 @@ export const MEMBER_ROLES_CELL_COMPONENT = "MemberRolesCell";
19
19
  // Admin (rank 2) shares the invite-create screen with TenantAdmin — TenantAdmin
20
20
  // must not appear here or Admin sees an unassignable option (fw#2414).
21
21
  export const DEFAULT_INVITE_ROLE_OPTIONS = ["User", "Editor", "Admin"];
22
- // Full assignable rank list, used by memberRolesEditScreen: that screen is
23
- // prefilled with a member's *current* roles, so it must be able to represent
24
- // every rank a membership can actually hold, including TenantAdmin (fw#2452).
22
+ // Full rank list for memberRolesEditScreen: it is prefilled with a member's
23
+ // *current* roles, so it must represent every rank a membership can hold,
24
+ // including TenantAdmin (fw#2452). The screen hides what the caller may not
25
+ // assign via TenantQueries.memberRoleAvailability.
25
26
  export const OWNER_INVITE_ROLE_OPTIONS = [
26
27
  "User",
27
28
  "Member",
@@ -58,6 +59,8 @@ export const TenantQueries = {
58
59
  invitations: "tenant:query:invitations",
59
60
  // Combined active-members + pending-invitations list backing /members.
60
61
  teamList: "tenant:query:team:list",
62
+ // Per-role assignability for the caller, mirrors the rank rule of updateMemberRoles.
63
+ memberRoleAvailability: "tenant:query:member-role-availability",
61
64
  };
62
65
  // Error codes
63
66
  export const TenantErrors = {
@@ -7,6 +7,7 @@ import { invitationsQuery } from "./handlers/invitations.query.js";
7
7
  import { listQuery } from "./handlers/list.query.js";
8
8
  import { meQuery } from "./handlers/me.query.js";
9
9
  import { memberDirectoryQuery } from "./handlers/member-directory.query.js";
10
+ import { memberRoleAvailabilityQuery } from "./handlers/member-role-availability.query.js";
10
11
  import { membersQuery } from "./handlers/members.query.js";
11
12
  import { membershipsQuery } from "./handlers/memberships.query.js";
12
13
  import { removeMemberWrite } from "./handlers/remove-member.write.js";
@@ -118,6 +119,7 @@ export function createTenantFeature(options) {
118
119
  resolveUserIds: r.queryHandler(resolveUserIdsQuery),
119
120
  invitations: r.queryHandler(invitationsQuery),
120
121
  teamList: r.queryHandler(teamListQuery),
122
+ memberRoleAvailability: r.queryHandler(memberRoleAvailabilityQuery),
121
123
  };
122
124
  // Entity-convention handlers for the SystemAdmin entityList/entityEdit
123
125
  // screens. The feature's original handlers predate the `<entity>:<verb>`
@@ -0,0 +1,7 @@
1
+ import * as z from "zod";
2
+ export declare const memberRoleAvailabilityQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"member-role-availability", z.ZodObject<{}, z.core.$strip>, {
3
+ rows: {
4
+ value: string;
5
+ disabled: boolean;
6
+ }[];
7
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,18 @@
1
+ import { access, assignableAppRolesOf, canActorAssignRole, defineQueryHandler, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { memberRoleOptions } from "../member-role-options.js";
4
+ export const memberRoleAvailabilityQuery = defineQueryHandler({
5
+ name: "member-role-availability",
6
+ schema: z.object({}),
7
+ access: { roles: access.admin },
8
+ description: "Lists every role the member roles form offers and whether the caller's own rank may assign it; the form hides the roles marked disabled.",
9
+ handler: async (query, ctx) => {
10
+ const assignableAppRoles = assignableAppRolesOf(ctx.registry);
11
+ return {
12
+ rows: memberRoleOptions(assignableAppRoles).map((value) => ({
13
+ value,
14
+ disabled: !canActorAssignRole(query.user.roles, value, assignableAppRoles),
15
+ })),
16
+ };
17
+ },
18
+ });
@@ -1,6 +1,6 @@
1
1
  import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createEventStoreExecutor, } from "@cosmicdrift/kumiko-framework/db";
3
- import { createSystemUser, defineWriteHandler, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createSystemUser, defineWriteHandler, parseTenantId, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
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";
@@ -20,7 +20,7 @@ async function cancelPendingInvitationsOfUser(db, options) {
20
20
  const email = (await decryptStoredPii(user.email, "email", "tenant:remove-member")).toLowerCase();
21
21
  const pendingInvitations = await selectMany(db, tenantInvitationsTable, { tenantId: options.tenantId, email, status: INVITATION_STATUS.pending });
22
22
  for (const invitation of pendingInvitations) {
23
- const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis);
23
+ const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis, options.streamTenantId);
24
24
  if (failure)
25
25
  return failure;
26
26
  }
@@ -43,11 +43,14 @@ export const removeMemberWrite = defineWriteHandler({
43
43
  });
44
44
  }
45
45
  const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin manages memberships across tenants");
46
- const existing = await fetchOne(db, tenantMembershipsTable, {
47
- userId: event.payload.userId,
48
- tenantId: event.payload.tenantId,
49
- });
50
- if (!existing) {
46
+ const streamTenantId = parseTenantId(event.payload.tenantId);
47
+ const existing = streamTenantId === null
48
+ ? undefined
49
+ : await fetchOne(db, tenantMembershipsTable, {
50
+ userId: event.payload.userId,
51
+ tenantId: streamTenantId,
52
+ });
53
+ if (!existing || streamTenantId === null) {
51
54
  return writeFailure(new NotFoundError("membership", undefined, {
52
55
  i18nKey: "tenant.errors.membershipNotFound",
53
56
  i18nParams: { userId: event.payload.userId, tenantId: event.payload.tenantId },
@@ -75,14 +78,14 @@ export const removeMemberWrite = defineWriteHandler({
75
78
  });
76
79
  }
77
80
  const result = await executor.delete({ id: existing["id"] }, // @cast-boundary db-row
78
- event.user, db);
81
+ event.user, db, { streamTenantId });
79
82
  if (!result.isSuccess)
80
83
  return result;
81
- // Actor tenant = invitation tenant so the update hits the invitation's stream.
82
84
  const cancelFailure = await cancelPendingInvitationsOfUser(db, {
83
85
  userId: event.payload.userId,
84
86
  tenantId: event.payload.tenantId,
85
- actor: { ...event.user, tenantId: event.payload.tenantId },
87
+ actor: event.user,
88
+ streamTenantId,
86
89
  redis: ctx.redis,
87
90
  });
88
91
  if (cancelFailure !== undefined)
@@ -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, assignableAppRolesOf, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, mergeAssignedRoles, SYSTEM_ROLE, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { access, assignableAppRolesOf, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, mayOverrideTenant, mergeAssignedRoles, parseTenantId, 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";
@@ -17,7 +17,12 @@ const REVOKE_ALL_SESSIONS_QN = "sessions:write:user-session:revoke-all-for-user"
17
17
  /** SystemAdmin may pass payload.tenantId (cross-tenant); the members actionForm
18
18
  * only sends userId+roles, so fall back to the active session tenant. */
19
19
  function resolveTargetTenantId(isSystem, payloadTenantId, sessionTenantId) {
20
- return isSystem ? (payloadTenantId ?? sessionTenantId) : sessionTenantId;
20
+ if (!isSystem || payloadTenantId === undefined)
21
+ return sessionTenantId;
22
+ return parseTenantId(payloadTenantId) ?? undefined;
23
+ }
24
+ function foreignStreamTenant(user, targetTenantId) {
25
+ return targetTenantId === user.tenantId ? undefined : targetTenantId;
21
26
  }
22
27
  // Editing your OWN roles keeps the current session: roles are re-derived per
23
28
  // request, so the new roles apply at once without a re-login. Other sessions
@@ -49,7 +54,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
49
54
  });
50
55
  }
51
56
  const isSystemActor = event.user.roles.includes(SYSTEM_ROLE);
52
- const isSystem = event.user.roles.includes("SystemAdmin") || isSystemActor;
57
+ const isSystem = mayOverrideTenant(event.user) || isSystemActor;
53
58
  const assignableAppRoles = assignableAppRolesOf(ctx.registry);
54
59
  const targetTenantId = resolveTargetTenantId(isSystem, event.payload.tenantId, event.user.tenantId);
55
60
  if (!targetTenantId) {
@@ -117,16 +122,14 @@ export const updateMemberRolesWrite = defineWriteHandler({
117
122
  if (revoker) {
118
123
  await ctx.writeAs(createSystemUser(targetTenantId), REVOKE_ALL_SESSIONS_QN, revokeSessionsPayload(event.payload.userId, targetTenantId, event.user));
119
124
  }
120
- // Stream tenant follows the actor via streamTenantFor — use the
121
- // membership tenant so SystemAdmin/cross-tenant and TenantAdmin paths
122
- // hit the same stream seedTenantMembership/invite-accept wrote.
123
- // Keep the real actor for audit metadata; only override tenantId so
124
- // streamTenantFor writes into the membership tenant (#2401).
125
+ // The membership stream lives on the membership's tenant (where seedTenantMembership/
126
+ // invite-accept wrote it); a cross-tenant operator addresses it without becoming a member.
127
+ // Same-tenant writes run on a tenant-mode db, which refuses a stream override.
125
128
  const result = await executor.update({
126
129
  id: row["id"], // @cast-boundary db-row
127
130
  version: row["version"], // @cast-boundary db-row
128
131
  changes: { roles: JSON.stringify(nextRoles) },
129
- }, { ...event.user, tenantId: targetTenantId }, db);
132
+ }, event.user, db, { streamTenantId: foreignStreamTenant(event.user, targetTenantId) });
130
133
  return withResponseData(result, {
131
134
  ...event.payload,
132
135
  roles: nextRoles,
@@ -12,6 +12,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
12
12
  readonly description?: string;
13
13
  readonly maxLength?: number;
14
14
  readonly minLength?: number;
15
+ readonly autoComplete?: string;
15
16
  readonly required?: boolean;
16
17
  readonly searchable?: boolean;
17
18
  readonly sortable?: boolean;
@@ -34,6 +35,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
34
35
  readonly description?: string;
35
36
  readonly maxLength?: number;
36
37
  readonly minLength?: number;
38
+ readonly autoComplete?: string;
37
39
  readonly required?: boolean;
38
40
  readonly searchable?: boolean;
39
41
  readonly sortable?: boolean;
@@ -81,6 +83,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
81
83
  readonly description?: string;
82
84
  readonly maxLength?: number;
83
85
  readonly minLength?: number;
86
+ readonly autoComplete?: string;
84
87
  readonly required?: boolean;
85
88
  readonly searchable?: boolean;
86
89
  readonly sortable?: boolean;
@@ -139,6 +142,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
139
142
  readonly description?: string;
140
143
  readonly maxLength?: number;
141
144
  readonly minLength?: number;
145
+ readonly autoComplete?: string;
142
146
  readonly required?: boolean;
143
147
  readonly searchable?: boolean;
144
148
  readonly sortable?: boolean;
@@ -161,6 +165,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
161
165
  readonly description?: string;
162
166
  readonly maxLength?: number;
163
167
  readonly minLength?: number;
168
+ readonly autoComplete?: string;
164
169
  readonly required?: boolean;
165
170
  readonly searchable?: boolean;
166
171
  readonly sortable?: boolean;
@@ -208,6 +213,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
208
213
  readonly description?: string;
209
214
  readonly maxLength?: number;
210
215
  readonly minLength?: number;
216
+ readonly autoComplete?: string;
211
217
  readonly required?: boolean;
212
218
  readonly searchable?: boolean;
213
219
  readonly sortable?: boolean;
@@ -0,0 +1,2 @@
1
+ import type { AssignableAppRoles } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function memberRoleOptions(assignableAppRoles: AssignableAppRoles): readonly string[];
@@ -0,0 +1,4 @@
1
+ import { OWNER_INVITE_ROLE_OPTIONS } from "./constants.js";
2
+ export function memberRoleOptions(assignableAppRoles) {
3
+ return [...OWNER_INVITE_ROLE_OPTIONS, ...assignableAppRoles.keys()];
4
+ }
@@ -4,6 +4,7 @@ export declare const tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
6
6
  readonly minLength?: number;
7
+ readonly autoComplete?: string;
7
8
  readonly required?: boolean;
8
9
  readonly searchable?: boolean;
9
10
  readonly sortable?: boolean;
@@ -26,6 +27,7 @@ export declare const tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
26
27
  readonly description?: string;
27
28
  readonly maxLength?: number;
28
29
  readonly minLength?: number;
30
+ readonly autoComplete?: string;
29
31
  readonly required?: boolean;
30
32
  readonly searchable?: boolean;
31
33
  readonly sortable?: boolean;
@@ -50,6 +52,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
50
52
  readonly description?: string;
51
53
  readonly maxLength?: number;
52
54
  readonly minLength?: number;
55
+ readonly autoComplete?: string;
53
56
  readonly required?: boolean;
54
57
  readonly searchable?: boolean;
55
58
  readonly sortable?: boolean;
@@ -72,6 +75,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
72
75
  readonly description?: string;
73
76
  readonly maxLength?: number;
74
77
  readonly minLength?: number;
78
+ readonly autoComplete?: string;
75
79
  readonly required?: boolean;
76
80
  readonly searchable?: boolean;
77
81
  readonly sortable?: boolean;
@@ -6,6 +6,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
6
6
  readonly description?: string;
7
7
  readonly maxLength?: number;
8
8
  readonly minLength?: number;
9
+ readonly autoComplete?: string;
9
10
  readonly required?: boolean;
10
11
  readonly searchable?: boolean;
11
12
  readonly sortable?: boolean;
@@ -28,6 +29,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
28
29
  readonly description?: string;
29
30
  readonly maxLength?: number;
30
31
  readonly minLength?: number;
32
+ readonly autoComplete?: string;
31
33
  readonly required?: boolean;
32
34
  readonly searchable?: boolean;
33
35
  readonly sortable?: boolean;
@@ -93,6 +95,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
93
95
  readonly description?: string;
94
96
  readonly maxLength?: number;
95
97
  readonly minLength?: number;
98
+ readonly autoComplete?: string;
96
99
  readonly required?: boolean;
97
100
  readonly searchable?: boolean;
98
101
  readonly sortable?: boolean;
@@ -162,6 +165,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
162
165
  readonly description?: string;
163
166
  readonly maxLength?: number;
164
167
  readonly minLength?: number;
168
+ readonly autoComplete?: string;
165
169
  readonly required?: boolean;
166
170
  readonly searchable?: boolean;
167
171
  readonly sortable?: boolean;
@@ -184,6 +188,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
184
188
  readonly description?: string;
185
189
  readonly maxLength?: number;
186
190
  readonly minLength?: number;
191
+ readonly autoComplete?: string;
187
192
  readonly required?: boolean;
188
193
  readonly searchable?: boolean;
189
194
  readonly sortable?: boolean;
@@ -249,6 +254,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
249
254
  readonly description?: string;
250
255
  readonly maxLength?: number;
251
256
  readonly minLength?: number;
257
+ readonly autoComplete?: string;
252
258
  readonly required?: boolean;
253
259
  readonly searchable?: boolean;
254
260
  readonly sortable?: boolean;
@@ -69,7 +69,8 @@ export declare function createMemberRolesEditScreen(assignableAppRoles?: Assigna
69
69
  };
70
70
  roles: {
71
71
  type: "multiSelect";
72
- options: string[];
72
+ options: readonly string[];
73
+ optionsAvailabilityQuery: "tenant:query:member-role-availability";
73
74
  required: true;
74
75
  };
75
76
  };
@@ -99,7 +100,8 @@ export declare const memberRolesEditScreen: {
99
100
  };
100
101
  roles: {
101
102
  type: "multiSelect";
102
- options: string[];
103
+ options: readonly string[];
104
+ optionsAvailabilityQuery: "tenant:query:member-role-availability";
103
105
  required: true;
104
106
  };
105
107
  };
@@ -1,6 +1,7 @@
1
1
  import { AuthHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/constants";
2
2
  import { access, i18nKey, isAssignableByRole, } from "@cosmicdrift/kumiko-framework/engine";
3
- import { DEFAULT_INVITE_ROLE_OPTIONS, INVITE_CREATE_SCREEN_ID, MEMBER_ROLES_CELL_COMPONENT, MEMBER_ROLES_EDIT_SCREEN_ID, MEMBER_STATUS_CELL_COMPONENT, MEMBERS_SCREEN_ID, OWNER_INVITE_ROLE_OPTIONS, TenantHandlers, TenantQueries, } from "./constants.js";
3
+ import { DEFAULT_INVITE_ROLE_OPTIONS, INVITE_CREATE_SCREEN_ID, MEMBER_ROLES_CELL_COMPONENT, MEMBER_ROLES_EDIT_SCREEN_ID, MEMBER_STATUS_CELL_COMPONENT, MEMBERS_SCREEN_ID, TenantHandlers, TenantQueries, } from "./constants.js";
4
+ import { memberRoleOptions } from "./member-role-options.js";
4
5
  // Cross-tenant SystemAdmin platform view of the tenants themselves. The tenant
5
6
  // feature runs with `r.systemScope()`, so the entityList returns every tenant.
6
7
  // Both screens are SystemAdmin-gated and inert until an app navs them.
@@ -83,6 +84,7 @@ export function createMembersScreen(options) {
83
84
  field: "lastSeenAt",
84
85
  label: i18nKey("tenant.members.col.lastActivity"),
85
86
  renderer: { format: "timestamp" },
87
+ hideOnNarrow: true,
86
88
  },
87
89
  ],
88
90
  defaultSort: { field: "createdAt", dir: "desc" },
@@ -172,13 +174,15 @@ export function createMemberRolesEditScreen(assignableAppRoles = new Map()) {
172
174
  cancelTarget: MEMBERS_SCREEN_ID,
173
175
  fields: {
174
176
  userId: { type: "text", required: true },
175
- // Prefilled with the member's current roles, so the option list must be
176
- // able to represent every assignable rank — DEFAULT_INVITE_ROLE_OPTIONS
177
- // omits TenantAdmin and would silently strip it on submit. Escalation
178
- // still stays server-side via findForbiddenRoleAssignment (update-member-roles.write.ts).
177
+ // Prefilled with the member's current roles, so the static options must
178
+ // represent every rank (DEFAULT_INVITE_ROLE_OPTIONS omits TenantAdmin and
179
+ // would silently strip it on submit); the availability query hides what the
180
+ // caller may not assign. Enforcement stays server-side via
181
+ // findForbiddenRoleAssignment (update-member-roles.write.ts).
179
182
  roles: {
180
183
  type: "multiSelect",
181
- options: [...OWNER_INVITE_ROLE_OPTIONS, ...assignableAppRoles.keys()],
184
+ options: memberRoleOptions(assignableAppRoles),
185
+ optionsAvailabilityQuery: TenantQueries.memberRoleAvailability,
182
186
  required: true,
183
187
  },
184
188
  },
@@ -4,6 +4,7 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
6
6
  readonly minLength?: number;
7
+ readonly autoComplete?: string;
7
8
  readonly required?: boolean;
8
9
  readonly searchable?: boolean;
9
10
  readonly sortable?: boolean;
@@ -26,6 +27,7 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
26
27
  readonly description?: string;
27
28
  readonly maxLength?: number;
28
29
  readonly minLength?: number;
30
+ readonly autoComplete?: string;
29
31
  readonly required?: boolean;
30
32
  readonly searchable?: boolean;
31
33
  readonly sortable?: boolean;