@cosmicdrift/kumiko-bundled-features 0.332.0 → 0.334.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 (53) hide show
  1. package/dist/audit/handlers/list.query.js +4 -3
  2. package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -1
  3. package/dist/auth-email-password/handlers/invite-create.write.js +2 -2
  4. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
  5. package/dist/billing-foundation/entities.d.ts +8 -0
  6. package/dist/billing-foundation/projection.d.ts +8 -0
  7. package/dist/cap-counter/entity.d.ts +1 -0
  8. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
  9. package/dist/config/table.d.ts +6 -0
  10. package/dist/config/web/i18n.js +12 -4
  11. package/dist/custom-fields/entity.d.ts +4 -0
  12. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
  13. package/dist/delivery/tables.d.ts +6 -0
  14. package/dist/document-ingest-foundation/entity.d.ts +4 -0
  15. package/dist/file-provider-s3/feature.js +1 -1
  16. package/dist/folders/entity.d.ts +8 -0
  17. package/dist/form-draft/entity.d.ts +2 -0
  18. package/dist/inbound-mail-foundation/entities.d.ts +25 -0
  19. package/dist/inbound-mail-foundation/projection.d.ts +20 -0
  20. package/dist/jobs/job-run-table.d.ts +6 -0
  21. package/dist/ledger/entity.d.ts +16 -3
  22. package/dist/managed-pages/table.d.ts +12 -0
  23. package/dist/notes-history/entity.d.ts +12 -0
  24. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
  25. package/dist/secrets/table.d.ts +1 -0
  26. package/dist/sessions/schema/user-session.d.ts +4 -0
  27. package/dist/subscription-stripe/feature.js +3 -3
  28. package/dist/tags/entity.d.ts +9 -0
  29. package/dist/template-resolver/table.d.ts +18 -8
  30. package/dist/template-resolver/user-content-table.d.ts +14 -4
  31. package/dist/tenant/constants.d.ts +1 -1
  32. package/dist/tenant/constants.js +7 -1
  33. package/dist/tenant/feature.d.ts +4 -1
  34. package/dist/tenant/feature.js +13 -4
  35. package/dist/tenant/handlers/update-member-roles.write.js +2 -2
  36. package/dist/tenant/i18n.js +2 -0
  37. package/dist/tenant/index.d.ts +1 -1
  38. package/dist/tenant/index.js +1 -1
  39. package/dist/tenant/invitation-table.d.ts +8 -2
  40. package/dist/tenant/membership-table.d.ts +4 -0
  41. package/dist/tenant/schema/tenant.d.ts +8 -2
  42. package/dist/tenant/screens.d.ts +59 -3
  43. package/dist/tenant/screens.js +54 -36
  44. package/dist/tenant-settings/feature.js +9 -6
  45. package/dist/tier-engine/entity.d.ts +2 -0
  46. package/dist/tier-engine/tier-resolver.d.ts +2 -0
  47. package/dist/user/schema/user.d.ts +14 -2
  48. package/dist/user-data-rights/run-user-export.js +6 -2
  49. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
  50. package/dist/user-data-rights/schema/download-token.d.ts +8 -0
  51. package/dist/user-data-rights/schema/export-job.d.ts +8 -2
  52. package/package.json +9 -9
  53. package/src/tenant/changes.json +5 -0
@@ -1,6 +1,6 @@
1
1
  export { TenantCommandSchemas } from "./command-schemas.js";
2
2
  export { TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries } from "./constants.js";
3
- export { createTenantFeature } from "./feature.js";
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";
6
6
  export { isTenantServingPublicContent } from "./is-tenant-serving-public-content.js";
@@ -1,6 +1,6 @@
1
1
  export { TenantCommandSchemas } from "./command-schemas.js";
2
2
  export { TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries } from "./constants.js";
3
- export { createTenantFeature } from "./feature.js";
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";
6
6
  export { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table.js";
@@ -17,6 +17,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
17
17
  readonly filterable?: boolean;
18
18
  readonly encrypted?: boolean;
19
19
  readonly sensitive?: boolean;
20
+ readonly writeOnly?: boolean;
20
21
  readonly readAsInstruction?: boolean;
21
22
  readonly format?: "email" | "url" | "phone" | "password";
22
23
  readonly default?: string;
@@ -37,6 +38,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
37
38
  readonly filterable?: boolean;
38
39
  readonly encrypted?: boolean;
39
40
  readonly sensitive?: boolean;
41
+ readonly writeOnly?: boolean;
40
42
  readonly readAsInstruction?: boolean;
41
43
  readonly format?: "email" | "url" | "phone" | "password";
42
44
  readonly default?: string;
@@ -65,7 +67,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
65
67
  readonly pending?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
66
68
  } | undefined;
67
69
  readonly optionsQuery?: string;
68
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
70
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
69
71
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
70
72
  required: true;
71
73
  };
@@ -79,6 +81,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
79
81
  readonly filterable?: boolean;
80
82
  readonly encrypted?: boolean;
81
83
  readonly sensitive?: boolean;
84
+ readonly writeOnly?: boolean;
82
85
  readonly readAsInstruction?: boolean;
83
86
  readonly format?: "email" | "url" | "phone" | "password";
84
87
  readonly default?: string;
@@ -116,6 +119,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
116
119
  readonly filterable?: boolean;
117
120
  readonly encrypted?: boolean;
118
121
  readonly sensitive?: boolean;
122
+ readonly writeOnly?: boolean;
119
123
  readonly readAsInstruction?: boolean;
120
124
  readonly format?: "email" | "url" | "phone" | "password";
121
125
  readonly default?: string;
@@ -136,6 +140,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
136
140
  readonly filterable?: boolean;
137
141
  readonly encrypted?: boolean;
138
142
  readonly sensitive?: boolean;
143
+ readonly writeOnly?: boolean;
139
144
  readonly readAsInstruction?: boolean;
140
145
  readonly format?: "email" | "url" | "phone" | "password";
141
146
  readonly default?: string;
@@ -164,7 +169,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
164
169
  readonly pending?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
165
170
  } | undefined;
166
171
  readonly optionsQuery?: string;
167
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
172
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
168
173
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
169
174
  required: true;
170
175
  };
@@ -178,6 +183,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
178
183
  readonly filterable?: boolean;
179
184
  readonly encrypted?: boolean;
180
185
  readonly sensitive?: boolean;
186
+ readonly writeOnly?: boolean;
181
187
  readonly readAsInstruction?: boolean;
182
188
  readonly format?: "email" | "url" | "phone" | "password";
183
189
  readonly default?: string;
@@ -9,6 +9,7 @@ export declare const tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
9
9
  readonly filterable?: boolean;
10
10
  readonly encrypted?: boolean;
11
11
  readonly sensitive?: boolean;
12
+ readonly writeOnly?: boolean;
12
13
  readonly readAsInstruction?: boolean;
13
14
  readonly format?: "email" | "url" | "phone" | "password";
14
15
  readonly default?: string;
@@ -29,6 +30,7 @@ export declare const tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
29
30
  readonly filterable?: boolean;
30
31
  readonly encrypted?: boolean;
31
32
  readonly sensitive?: boolean;
33
+ readonly writeOnly?: boolean;
32
34
  readonly readAsInstruction?: boolean;
33
35
  readonly format?: "email" | "url" | "phone" | "password";
34
36
  readonly default?: string;
@@ -51,6 +53,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
51
53
  readonly filterable?: boolean;
52
54
  readonly encrypted?: boolean;
53
55
  readonly sensitive?: boolean;
56
+ readonly writeOnly?: boolean;
54
57
  readonly readAsInstruction?: boolean;
55
58
  readonly format?: "email" | "url" | "phone" | "password";
56
59
  readonly default?: string;
@@ -71,6 +74,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
71
74
  readonly filterable?: boolean;
72
75
  readonly encrypted?: boolean;
73
76
  readonly sensitive?: boolean;
77
+ readonly writeOnly?: boolean;
74
78
  readonly readAsInstruction?: boolean;
75
79
  readonly format?: "email" | "url" | "phone" | "password";
76
80
  readonly default?: string;
@@ -11,6 +11,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
11
11
  readonly filterable?: boolean;
12
12
  readonly encrypted?: boolean;
13
13
  readonly sensitive?: boolean;
14
+ readonly writeOnly?: boolean;
14
15
  readonly readAsInstruction?: boolean;
15
16
  readonly format?: "email" | "url" | "phone" | "password";
16
17
  readonly default?: string;
@@ -31,6 +32,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
31
32
  readonly filterable?: boolean;
32
33
  readonly encrypted?: boolean;
33
34
  readonly sensitive?: boolean;
35
+ readonly writeOnly?: boolean;
34
36
  readonly readAsInstruction?: boolean;
35
37
  readonly format?: "email" | "url" | "phone" | "password";
36
38
  readonly default?: string;
@@ -63,7 +65,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
63
65
  readonly destroying?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
64
66
  } | undefined;
65
67
  readonly optionsQuery?: string;
66
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
68
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
67
69
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
68
70
  required: true;
69
71
  };
@@ -92,6 +94,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
92
94
  readonly filterable?: boolean;
93
95
  readonly encrypted?: boolean;
94
96
  readonly sensitive?: boolean;
97
+ readonly writeOnly?: boolean;
95
98
  readonly readAsInstruction?: boolean;
96
99
  readonly format?: "email" | "url" | "phone" | "password";
97
100
  readonly default?: string;
@@ -159,6 +162,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
159
162
  readonly filterable?: boolean;
160
163
  readonly encrypted?: boolean;
161
164
  readonly sensitive?: boolean;
165
+ readonly writeOnly?: boolean;
162
166
  readonly readAsInstruction?: boolean;
163
167
  readonly format?: "email" | "url" | "phone" | "password";
164
168
  readonly default?: string;
@@ -179,6 +183,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
179
183
  readonly filterable?: boolean;
180
184
  readonly encrypted?: boolean;
181
185
  readonly sensitive?: boolean;
186
+ readonly writeOnly?: boolean;
182
187
  readonly readAsInstruction?: boolean;
183
188
  readonly format?: "email" | "url" | "phone" | "password";
184
189
  readonly default?: string;
@@ -211,7 +216,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
211
216
  readonly destroying?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
212
217
  } | undefined;
213
218
  readonly optionsQuery?: string;
214
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
219
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
215
220
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
216
221
  required: true;
217
222
  };
@@ -240,6 +245,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
240
245
  readonly filterable?: boolean;
241
246
  readonly encrypted?: boolean;
242
247
  readonly sensitive?: boolean;
248
+ readonly writeOnly?: boolean;
243
249
  readonly readAsInstruction?: boolean;
244
250
  readonly format?: "email" | "url" | "phone" | "password";
245
251
  readonly default?: string;
@@ -1,9 +1,35 @@
1
- import { type EntityEditScreenDefinition, type EntityListScreenDefinition, type ScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { type AssignableAppRoles, type EntityEditScreenDefinition, type EntityListScreenDefinition, type ScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const tenantListScreen: EntityListScreenDefinition;
3
3
  export declare const tenantEditScreen: EntityEditScreenDefinition;
4
4
  export declare function createMembersScreen(options?: {
5
5
  readonly inviteScreen?: boolean;
6
6
  }): ScreenDefinition;
7
+ export declare function createInviteCreateScreen(assignableAppRoles?: AssignableAppRoles): {
8
+ id: "invite-create";
9
+ type: "actionForm";
10
+ handler: "auth-email-password:write:invite-create";
11
+ fields: {
12
+ email: {
13
+ type: "text";
14
+ required: true;
15
+ format: "email";
16
+ };
17
+ role: {
18
+ type: "select";
19
+ options: string[];
20
+ required: true;
21
+ };
22
+ };
23
+ layout: {
24
+ sections: {
25
+ fields: string[];
26
+ }[];
27
+ };
28
+ submitLabel: string;
29
+ access: {
30
+ roles: readonly string[];
31
+ };
32
+ };
7
33
  export declare const inviteCreateScreen: {
8
34
  id: "invite-create";
9
35
  type: "actionForm";
@@ -16,7 +42,7 @@ export declare const inviteCreateScreen: {
16
42
  };
17
43
  role: {
18
44
  type: "select";
19
- options: readonly ["User", "Editor", "Admin"];
45
+ options: string[];
20
46
  required: true;
21
47
  };
22
48
  };
@@ -30,6 +56,36 @@ export declare const inviteCreateScreen: {
30
56
  roles: readonly string[];
31
57
  };
32
58
  };
59
+ export declare function createMemberRolesEditScreen(assignableAppRoles?: AssignableAppRoles): {
60
+ id: "member-roles-edit";
61
+ type: "actionForm";
62
+ handler: "tenant:write:update-member-roles";
63
+ redirect: "members";
64
+ cancelTarget: "members";
65
+ fields: {
66
+ userId: {
67
+ type: "text";
68
+ required: true;
69
+ };
70
+ roles: {
71
+ type: "multiSelect";
72
+ options: string[];
73
+ required: true;
74
+ };
75
+ };
76
+ layout: {
77
+ sections: {
78
+ fields: (string | {
79
+ field: string;
80
+ readOnly: true;
81
+ })[];
82
+ }[];
83
+ };
84
+ submitLabel: string;
85
+ access: {
86
+ roles: readonly string[];
87
+ };
88
+ };
33
89
  export declare const memberRolesEditScreen: {
34
90
  id: "member-roles-edit";
35
91
  type: "actionForm";
@@ -43,7 +99,7 @@ export declare const memberRolesEditScreen: {
43
99
  };
44
100
  roles: {
45
101
  type: "multiSelect";
46
- options: readonly ["User", "Editor", "Admin", "TenantAdmin"];
102
+ options: string[];
47
103
  required: true;
48
104
  };
49
105
  };
@@ -1,5 +1,5 @@
1
1
  import { AuthHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/constants";
2
- import { access, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { access, i18nKey, isAssignableByRole, } from "@cosmicdrift/kumiko-framework/engine";
3
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";
4
4
  // Cross-tenant SystemAdmin platform view of the tenants themselves. The tenant
5
5
  // feature runs with `r.systemScope()`, so the entityList returns every tenant.
@@ -138,38 +138,56 @@ export function createMembersScreen(options) {
138
138
  // only entry point is the drawer button on /members. Only registered by
139
139
  // feature.ts when `TenantFeatureOptions.inviteScreen` is set — see
140
140
  // createMembersScreen's doc comment above for why.
141
- export const inviteCreateScreen = {
142
- id: INVITE_CREATE_SCREEN_ID,
143
- type: "actionForm",
144
- handler: AuthHandlers.inviteCreate,
145
- fields: {
146
- email: { type: "text", required: true, format: "email" },
147
- role: { type: "select", options: DEFAULT_INVITE_ROLE_OPTIONS, required: true },
148
- },
149
- layout: {
150
- sections: [{ fields: ["email", "role"] }],
151
- },
152
- submitLabel: i18nKey("tenant.members.invite.submit"),
153
- access: { roles: access.admin },
154
- };
155
- export const memberRolesEditScreen = {
156
- id: MEMBER_ROLES_EDIT_SCREEN_ID,
157
- type: "actionForm",
158
- handler: TenantHandlers.updateMemberRoles,
159
- redirect: MEMBERS_SCREEN_ID,
160
- cancelTarget: MEMBERS_SCREEN_ID,
161
- fields: {
162
- userId: { type: "text", required: true },
163
- // Prefilled with the member's current roles, so the option list must be
164
- // able to represent every assignable rank — DEFAULT_INVITE_ROLE_OPTIONS
165
- // omits TenantAdmin and would silently strip it on submit. Escalation
166
- // still stays server-side via findForbiddenRoleAssignment (update-member-roles.write.ts).
167
- roles: { type: "multiSelect", options: OWNER_INVITE_ROLE_OPTIONS, required: true },
168
- },
169
- layout: {
170
- // Prefill userId from rowAction; readOnly so the operator cannot retarget.
171
- sections: [{ fields: [{ field: "userId", readOnly: true }, "roles"] }],
172
- },
173
- submitLabel: i18nKey("tenant.members.roles.edit.submit"),
174
- access: { roles: access.admin },
175
- };
141
+ export function createInviteCreateScreen(assignableAppRoles = new Map()) {
142
+ // Admin sees this screen too, so only app roles Admin may grant are offered (fw#2414).
143
+ const appRoleOptions = [...assignableAppRoles]
144
+ .filter(([, assignableFrom]) => isAssignableByRole(assignableFrom, "Admin"))
145
+ .map(([role]) => role);
146
+ return {
147
+ id: INVITE_CREATE_SCREEN_ID,
148
+ type: "actionForm",
149
+ handler: AuthHandlers.inviteCreate,
150
+ fields: {
151
+ email: { type: "text", required: true, format: "email" },
152
+ role: {
153
+ type: "select",
154
+ options: [...DEFAULT_INVITE_ROLE_OPTIONS, ...appRoleOptions],
155
+ required: true,
156
+ },
157
+ },
158
+ layout: {
159
+ sections: [{ fields: ["email", "role"] }],
160
+ },
161
+ submitLabel: i18nKey("tenant.members.invite.submit"),
162
+ access: { roles: access.admin },
163
+ };
164
+ }
165
+ export const inviteCreateScreen = createInviteCreateScreen();
166
+ export function createMemberRolesEditScreen(assignableAppRoles = new Map()) {
167
+ return {
168
+ id: MEMBER_ROLES_EDIT_SCREEN_ID,
169
+ type: "actionForm",
170
+ handler: TenantHandlers.updateMemberRoles,
171
+ redirect: MEMBERS_SCREEN_ID,
172
+ cancelTarget: MEMBERS_SCREEN_ID,
173
+ fields: {
174
+ 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).
179
+ roles: {
180
+ type: "multiSelect",
181
+ options: [...OWNER_INVITE_ROLE_OPTIONS, ...assignableAppRoles.keys()],
182
+ required: true,
183
+ },
184
+ },
185
+ layout: {
186
+ // Prefill userId from rowAction; readOnly so the operator cannot retarget.
187
+ sections: [{ fields: [{ field: "userId", readOnly: true }, "roles"] }],
188
+ },
189
+ submitLabel: i18nKey("tenant.members.roles.edit.submit"),
190
+ access: { roles: access.admin },
191
+ };
192
+ }
193
+ export const memberRolesEditScreen = createMemberRolesEditScreen();
@@ -21,21 +21,24 @@ export function createTenantSettingsFeature(opts = {}) {
21
21
  r.config({ keys: buildTenantSettingsKeys(opts) });
22
22
  r.translations({
23
23
  keys: {
24
- "tenant-settings.currency": { en: "Default Currency" },
25
- "tenant-settings.locale": { en: "Default Locale" },
24
+ "tenant-settings.currency": { en: "Default currency" },
25
+ "tenant-settings.locale": { en: "Default locale" },
26
26
  // Settings-Hub-derived configEdit screen title (buildConfigFeatureSchema) —
27
27
  // required by the i18n boot-validator whenever a feature masks config keys.
28
- "screen:tenant-settings-system.title": { en: "Tenant Settings" },
29
- "screen:tenant-settings-tenant.title": { en: "Tenant Settings" },
28
+ "screen:tenant-settings-system.title": { en: "Tenant defaults" },
29
+ "screen:tenant-settings-tenant.title": { en: "Tenant settings" },
30
30
  // Settings-Hub nav label + configEdit section heading (same key, both
31
31
  // scopes fall back here — see buildConfigFeatureSchema).
32
- "tenant-settings.settings": { en: "Tenant Settings" },
32
+ "tenant-settings.settings": { en: "Tenant settings" },
33
+ // Section heading below the page title (the page title is the same words as
34
+ // `.settings`, which RenderEdit would drop as a repeat).
35
+ "tenant-settings.settings.section": { en: "Currency and language" },
33
36
  // Both currency/locale are tenant-home with admin write access, which
34
37
  // cascades a SystemAdmin-only "set the platform default" screen up to
35
38
  // the system audience — scoped override so its nav entry reads
36
39
  // differently from the tenant-home entry instead of repeating the
37
40
  // same words under both "Platform" and "Organization".
38
- "tenant-settings.settings.system": { en: "Tenant Settings (Platform Default)" },
41
+ "tenant-settings.settings.system": { en: "Tenant defaults" },
39
42
  },
40
43
  });
41
44
  });
@@ -9,6 +9,7 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
9
9
  readonly filterable?: boolean;
10
10
  readonly encrypted?: boolean;
11
11
  readonly sensitive?: boolean;
12
+ readonly writeOnly?: boolean;
12
13
  readonly readAsInstruction?: boolean;
13
14
  readonly format?: "email" | "url" | "phone" | "password";
14
15
  readonly default?: string;
@@ -29,6 +30,7 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
29
30
  readonly filterable?: boolean;
30
31
  readonly encrypted?: boolean;
31
32
  readonly sensitive?: boolean;
33
+ readonly writeOnly?: boolean;
32
34
  readonly readAsInstruction?: boolean;
33
35
  readonly format?: "email" | "url" | "phone" | "password";
34
36
  readonly default?: string;
@@ -17,6 +17,7 @@ export declare function createTierResolver<TTier extends string, TCaps>(deps: Ti
17
17
  readonly filterable?: boolean;
18
18
  readonly encrypted?: boolean;
19
19
  readonly sensitive?: boolean;
20
+ readonly writeOnly?: boolean;
20
21
  readonly readAsInstruction?: boolean;
21
22
  readonly format?: "email" | "url" | "phone" | "password";
22
23
  readonly default?: string;
@@ -37,6 +38,7 @@ export declare function createTierResolver<TTier extends string, TCaps>(deps: Ti
37
38
  readonly filterable?: boolean;
38
39
  readonly encrypted?: boolean;
39
40
  readonly sensitive?: boolean;
41
+ readonly writeOnly?: boolean;
40
42
  readonly readAsInstruction?: boolean;
41
43
  readonly format?: "email" | "url" | "phone" | "password";
42
44
  readonly default?: string;
@@ -38,6 +38,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
38
38
  readonly filterable?: boolean;
39
39
  readonly encrypted?: boolean;
40
40
  readonly sensitive?: boolean;
41
+ readonly writeOnly?: boolean;
41
42
  readonly readAsInstruction?: boolean;
42
43
  readonly format?: "email" | "url" | "phone" | "password";
43
44
  readonly default?: string;
@@ -58,6 +59,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
58
59
  readonly filterable?: boolean;
59
60
  readonly encrypted?: boolean;
60
61
  readonly sensitive?: boolean;
62
+ readonly writeOnly?: boolean;
61
63
  readonly readAsInstruction?: boolean;
62
64
  readonly format?: "email" | "url" | "phone" | "password";
63
65
  readonly default?: string;
@@ -78,6 +80,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
78
80
  readonly filterable?: boolean;
79
81
  readonly encrypted?: boolean;
80
82
  readonly sensitive?: boolean;
83
+ readonly writeOnly?: boolean;
81
84
  readonly readAsInstruction?: boolean;
82
85
  readonly format?: "email" | "url" | "phone" | "password";
83
86
  readonly default?: string;
@@ -98,6 +101,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
98
101
  readonly filterable?: boolean;
99
102
  readonly encrypted?: boolean;
100
103
  readonly sensitive?: boolean;
104
+ readonly writeOnly?: boolean;
101
105
  readonly readAsInstruction?: boolean;
102
106
  readonly format?: "email" | "url" | "phone" | "password";
103
107
  readonly default?: string;
@@ -127,6 +131,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
127
131
  readonly filterable?: boolean;
128
132
  readonly encrypted?: boolean;
129
133
  readonly sensitive?: boolean;
134
+ readonly writeOnly?: boolean;
130
135
  readonly readAsInstruction?: boolean;
131
136
  readonly format?: "email" | "url" | "phone" | "password";
132
137
  readonly default?: string;
@@ -159,7 +164,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
159
164
  readonly restricted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
160
165
  } | undefined;
161
166
  readonly optionsQuery?: string;
162
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
167
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
163
168
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
164
169
  required: true;
165
170
  };
@@ -188,6 +193,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
188
193
  readonly filterable?: boolean;
189
194
  readonly encrypted?: boolean;
190
195
  readonly sensitive?: boolean;
196
+ readonly writeOnly?: boolean;
191
197
  readonly readAsInstruction?: boolean;
192
198
  readonly format?: "email" | "url" | "phone" | "password";
193
199
  readonly default?: string;
@@ -210,6 +216,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
210
216
  readonly filterable?: boolean;
211
217
  readonly encrypted?: boolean;
212
218
  readonly sensitive?: boolean;
219
+ readonly writeOnly?: boolean;
213
220
  readonly readAsInstruction?: boolean;
214
221
  readonly format?: "email" | "url" | "phone" | "password";
215
222
  readonly default?: string;
@@ -230,6 +237,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
230
237
  readonly filterable?: boolean;
231
238
  readonly encrypted?: boolean;
232
239
  readonly sensitive?: boolean;
240
+ readonly writeOnly?: boolean;
233
241
  readonly readAsInstruction?: boolean;
234
242
  readonly format?: "email" | "url" | "phone" | "password";
235
243
  readonly default?: string;
@@ -250,6 +258,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
250
258
  readonly filterable?: boolean;
251
259
  readonly encrypted?: boolean;
252
260
  readonly sensitive?: boolean;
261
+ readonly writeOnly?: boolean;
253
262
  readonly readAsInstruction?: boolean;
254
263
  readonly format?: "email" | "url" | "phone" | "password";
255
264
  readonly default?: string;
@@ -270,6 +279,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
270
279
  readonly filterable?: boolean;
271
280
  readonly encrypted?: boolean;
272
281
  readonly sensitive?: boolean;
282
+ readonly writeOnly?: boolean;
273
283
  readonly readAsInstruction?: boolean;
274
284
  readonly format?: "email" | "url" | "phone" | "password";
275
285
  readonly default?: string;
@@ -299,6 +309,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
299
309
  readonly filterable?: boolean;
300
310
  readonly encrypted?: boolean;
301
311
  readonly sensitive?: boolean;
312
+ readonly writeOnly?: boolean;
302
313
  readonly readAsInstruction?: boolean;
303
314
  readonly format?: "email" | "url" | "phone" | "password";
304
315
  readonly default?: string;
@@ -331,7 +342,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
331
342
  readonly restricted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
332
343
  } | undefined;
333
344
  readonly optionsQuery?: string;
334
- readonly optionsQueryPayload?: Readonly<Record<string, string | number | boolean>>;
345
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
335
346
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
336
347
  required: true;
337
348
  };
@@ -360,6 +371,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
360
371
  readonly filterable?: boolean;
361
372
  readonly encrypted?: boolean;
362
373
  readonly sensitive?: boolean;
374
+ readonly writeOnly?: boolean;
363
375
  readonly readAsInstruction?: boolean;
364
376
  readonly format?: "email" | "url" | "phone" | "password";
365
377
  readonly default?: string;
@@ -22,7 +22,7 @@
22
22
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
23
23
  import { configuredPiiSubjectKms, decryptPiiFieldValues, isPiiCiphertext, } from "@cosmicdrift/kumiko-framework/crypto";
24
24
  import { collectEncryptedFieldNames, configuredEntityFieldEncryption, createTenantDb, decryptEntityFieldValues, } from "@cosmicdrift/kumiko-framework/db";
25
- import { EXT_USER_DATA, extensionUsageEscapeHatchReason, } from "@cosmicdrift/kumiko-framework/engine";
25
+ import { EXT_USER_DATA, extensionUsageEscapeHatchReason, maskWriteOnlyFields, } from "@cosmicdrift/kumiko-framework/engine";
26
26
  import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
27
27
  import { tenantMembershipsTable } from "../tenant/index.js";
28
28
  import { buildFileRefZipPath } from "./zip-path.js";
@@ -214,7 +214,11 @@ async function decryptSnippetFields(registry, hookEntityName, snippet) {
214
214
  const entity = registry.getEntity(snippet.entity) ?? registry.getEntity(hookEntityName);
215
215
  const encryptedFields = entity ? collectEncryptedFieldNames(entity) : new Set();
216
216
  const kms = configuredPiiSubjectKms();
217
- const rows = await decryptRecords(snippet.rows, encryptedFields, kms);
217
+ const decryptedRows = await decryptRecords(snippet.rows, encryptedFields, kms);
218
+ // Decrypting would otherwise hand writeOnly secrets out as plaintext.
219
+ const rows = entity
220
+ ? decryptedRows.map((row) => maskWriteOnlyFields(entity, row))
221
+ : decryptedRows;
218
222
  if (!snippet.fileRefs || snippet.fileRefs.length === 0) {
219
223
  return { ...snippet, rows };
220
224
  }