@cosmicdrift/kumiko-bundled-features 0.351.0 → 0.353.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 (164) hide show
  1. package/dist/agent-tools/agent-manifest.js +8 -2
  2. package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
  3. package/dist/agent-tools/manifest-deny-qns.js +17 -0
  4. package/dist/agent-tools/tool-catalog.d.ts +2 -1
  5. package/dist/agent-tools/tool-catalog.js +4 -1
  6. package/dist/audit/handlers/list.query.js +13 -8
  7. package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -3
  8. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
  9. package/dist/auth-mfa/screens.js +2 -2
  10. package/dist/billing-foundation/checkout-core.d.ts +7 -0
  11. package/dist/billing-foundation/checkout-core.js +7 -2
  12. package/dist/billing-foundation/entities.d.ts +8 -0
  13. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
  14. package/dist/billing-foundation/projection.d.ts +8 -0
  15. package/dist/cap-counter/book-cap-usage.d.ts +5 -1
  16. package/dist/cap-counter/book-cap-usage.js +65 -44
  17. package/dist/cap-counter/cap-reservation.d.ts +12 -0
  18. package/dist/cap-counter/cap-reservation.js +135 -0
  19. package/dist/cap-counter/constants.d.ts +3 -0
  20. package/dist/cap-counter/constants.js +6 -0
  21. package/dist/cap-counter/entity.d.ts +1 -0
  22. package/dist/cap-counter/feature.js +7 -2
  23. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +5 -0
  24. package/dist/cap-counter/handlers/increment-rolling.write.js +4 -0
  25. package/dist/cap-counter/index.d.ts +4 -2
  26. package/dist/cap-counter/index.js +3 -1
  27. package/dist/cap-counter/payload-field.d.ts +1 -0
  28. package/dist/cap-counter/payload-field.js +11 -0
  29. package/dist/cap-counter/reservation-chain.d.ts +2 -0
  30. package/dist/cap-counter/reservation-chain.js +44 -0
  31. package/dist/cap-counter/stock-cap-guard.d.ts +3 -0
  32. package/dist/cap-counter/tables.d.ts +3 -0
  33. package/dist/cap-counter/tables.js +36 -0
  34. package/dist/cap-counter/value-cap-guard.d.ts +18 -0
  35. package/dist/cap-counter/value-cap-guard.js +33 -0
  36. package/dist/cap-counter/with-cap-enforcement.d.ts +5 -7
  37. package/dist/cap-counter/with-cap-enforcement.js +35 -51
  38. package/dist/cap-overview/access.js +2 -9
  39. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
  40. package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
  41. package/dist/channel-discord/feature.js +1 -0
  42. package/dist/channel-slack/feature.js +1 -0
  43. package/dist/channel-teams/feature.js +1 -0
  44. package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
  45. package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
  46. package/dist/config/handlers/values.query.d.ts +3 -1
  47. package/dist/config/handlers/values.query.js +7 -2
  48. package/dist/config/table.d.ts +6 -0
  49. package/dist/custom-fields/entity.d.ts +4 -0
  50. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
  51. package/dist/delivery/attempt-log-retention.d.ts +9 -0
  52. package/dist/delivery/attempt-log-retention.js +23 -0
  53. package/dist/delivery/attempt-log.d.ts +1 -1
  54. package/dist/delivery/attempt-log.js +21 -2
  55. package/dist/delivery/constants.d.ts +1 -1
  56. package/dist/delivery/constants.js +1 -1
  57. package/dist/delivery/events.d.ts +1 -0
  58. package/dist/delivery/events.js +2 -0
  59. package/dist/delivery/feature.d.ts +1 -0
  60. package/dist/delivery/feature.js +39 -3
  61. package/dist/delivery/index.d.ts +2 -1
  62. package/dist/delivery/index.js +2 -1
  63. package/dist/delivery/mask-recipient-address.d.ts +1 -0
  64. package/dist/delivery/mask-recipient-address.js +41 -0
  65. package/dist/delivery/public-names.d.ts +2 -0
  66. package/dist/delivery/public-names.js +4 -1
  67. package/dist/delivery/tables.d.ts +6 -0
  68. package/dist/delivery/types.d.ts +1 -0
  69. package/dist/delivery/types.js +6 -2
  70. package/dist/document-ingest-foundation/entity.d.ts +4 -0
  71. package/dist/files-tenant-data/hooks.js +3 -2
  72. package/dist/folders/entity.d.ts +8 -0
  73. package/dist/folders/feature.js +2 -10
  74. package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
  75. package/dist/form-draft/db/queries/cleanup.js +4 -3
  76. package/dist/form-draft/entity.d.ts +2 -0
  77. package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
  78. package/dist/form-draft/handlers/cleanup.job.js +11 -5
  79. package/dist/inbound-mail-foundation/entities.d.ts +25 -0
  80. package/dist/inbound-mail-foundation/projection.d.ts +20 -0
  81. package/dist/jobs/handlers/list.query.d.ts +2 -1
  82. package/dist/jobs/handlers/list.query.js +39 -14
  83. package/dist/jobs/i18n.js +1 -0
  84. package/dist/jobs/job-run-table.d.ts +6 -0
  85. package/dist/ledger/entity.d.ts +24 -8
  86. package/dist/ledger/entity.js +4 -0
  87. package/dist/ledger/schemas.js +8 -2
  88. package/dist/managed-pages/feature.js +28 -10
  89. package/dist/managed-pages/table.d.ts +12 -0
  90. package/dist/notes-history/entity.d.ts +12 -0
  91. package/dist/notes-history/feature.js +2 -10
  92. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
  93. package/dist/personal-access-tokens/screens.js +14 -3
  94. package/dist/renderer-foundation/types.d.ts +1 -0
  95. package/dist/renderer-simple/feature.js +4 -6
  96. package/dist/renderer-simple/index.d.ts +1 -0
  97. package/dist/renderer-simple/mail-blocks.d.ts +21 -0
  98. package/dist/renderer-simple/mail-blocks.js +107 -0
  99. package/dist/renderer-simple/simple-renderer.js +21 -3
  100. package/dist/secrets/table.d.ts +1 -0
  101. package/dist/seo/feature.js +28 -6
  102. package/dist/sessions/schema/user-session.d.ts +4 -0
  103. package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
  104. package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
  105. package/dist/shared/index.d.ts +2 -1
  106. package/dist/shared/index.js +2 -1
  107. package/dist/shared/rate-limited-text-response.d.ts +2 -0
  108. package/dist/shared/rate-limited-text-response.js +9 -0
  109. package/dist/tags/entity.d.ts +9 -0
  110. package/dist/tags/feature.js +2 -10
  111. package/dist/template-resolver/table.d.ts +48 -30
  112. package/dist/template-resolver/user-content-table.d.ts +34 -20
  113. package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
  114. package/dist/tenant/decrypt-tenant-names.js +10 -0
  115. package/dist/tenant/handlers/me.query.js +5 -1
  116. package/dist/tenant/handlers/memberships.query.js +3 -2
  117. package/dist/tenant/handlers/tenant-directory.query.js +9 -3
  118. package/dist/tenant/index.d.ts +1 -0
  119. package/dist/tenant/index.js +1 -0
  120. package/dist/tenant/invitation-table.d.ts +16 -8
  121. package/dist/tenant/membership-table.d.ts +4 -0
  122. package/dist/tenant/schema/tenant.d.ts +18 -10
  123. package/dist/tenant/schema/tenant.js +4 -6
  124. package/dist/tenant/screens.js +1 -1
  125. package/dist/tenant-handover/move-entity-graph.js +11 -18
  126. package/dist/tenant-lifecycle/feature.js +1 -0
  127. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +3 -0
  128. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -0
  129. package/dist/tenant-lifecycle/stages.d.ts +2 -0
  130. package/dist/tenant-lifecycle/stages.js +7 -1
  131. package/dist/tier-engine/entity.d.ts +2 -0
  132. package/dist/tier-engine/index.d.ts +1 -0
  133. package/dist/tier-engine/index.js +1 -0
  134. package/dist/tier-engine/tier-option-gate.d.ts +32 -0
  135. package/dist/tier-engine/tier-option-gate.js +62 -0
  136. package/dist/tier-engine/tier-resolver.d.ts +2 -0
  137. package/dist/user/handlers/list.query.js +2 -2
  138. package/dist/user/schema/user.d.ts +22 -8
  139. package/dist/user-data-rights/boot-checks.js +5 -0
  140. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
  141. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
  142. package/dist/user-data-rights/schema/download-token.d.ts +8 -0
  143. package/dist/user-data-rights/schema/export-job.d.ts +16 -8
  144. package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +25 -6
  145. package/dist/waitlist/entity.d.ts +22 -6
  146. package/dist/workflow-runner/event-trigger.js +6 -3
  147. package/dist/workflow-runner/handlers/resume-run.write.js +3 -0
  148. package/package.json +10 -10
  149. package/src/agent-tools/changes.json +12 -0
  150. package/src/audit/changes.json +6 -0
  151. package/src/cap-counter/changes.json +28 -0
  152. package/src/delivery/changes.json +12 -0
  153. package/src/derivatives-sharp/changes.json +6 -0
  154. package/src/form-draft/changes.json +7 -1
  155. package/src/legal-pages/changes.json +6 -0
  156. package/src/managed-pages/changes.json +6 -0
  157. package/src/renderer-simple/changes.json +6 -0
  158. package/src/seo/changes.json +5 -0
  159. package/src/tenant/changes.json +7 -0
  160. package/src/tenant-handover/changes.json +6 -0
  161. package/src/tier-engine/changes.json +6 -0
  162. package/src/workflow-runner/changes.json +5 -0
  163. package/dist/shared/has-where-rule.d.ts +0 -2
  164. package/dist/shared/has-where-rule.js +0 -10
@@ -5,6 +5,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
5
5
  readonly type: "text";
6
6
  readonly description?: string;
7
7
  readonly maxLength?: number;
8
+ readonly minLength?: number;
8
9
  readonly required?: boolean;
9
10
  readonly searchable?: boolean;
10
11
  readonly sortable?: boolean;
@@ -26,6 +27,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
26
27
  readonly type: "text";
27
28
  readonly description?: string;
28
29
  readonly maxLength?: number;
30
+ readonly minLength?: number;
29
31
  readonly required?: boolean;
30
32
  readonly searchable?: boolean;
31
33
  readonly sortable?: boolean;
@@ -58,14 +60,15 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
58
60
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
59
61
  readonly display?: "radio" | "dropdown";
60
62
  readonly optionTones?: {
61
- readonly active?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
62
- readonly destroyFailed?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
63
- readonly destroyRequested?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
64
- readonly destroyed?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
65
- readonly destroying?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
63
+ readonly active?: "bad" | "neutral" | "ok" | "warn" | undefined;
64
+ readonly destroyFailed?: "bad" | "neutral" | "ok" | "warn" | undefined;
65
+ readonly destroyRequested?: "bad" | "neutral" | "ok" | "warn" | undefined;
66
+ readonly destroyed?: "bad" | "neutral" | "ok" | "warn" | undefined;
67
+ readonly destroying?: "bad" | "neutral" | "ok" | "warn" | undefined;
66
68
  } | undefined;
67
69
  readonly optionsQuery?: string;
68
70
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
71
+ readonly optionsAvailabilityQuery?: string;
69
72
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
70
73
  required: true;
71
74
  };
@@ -88,6 +91,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
88
91
  readonly type: "text";
89
92
  readonly description?: string;
90
93
  readonly maxLength?: number;
94
+ readonly minLength?: number;
91
95
  readonly required?: boolean;
92
96
  readonly searchable?: boolean;
93
97
  readonly sortable?: boolean;
@@ -156,6 +160,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
156
160
  readonly type: "text";
157
161
  readonly description?: string;
158
162
  readonly maxLength?: number;
163
+ readonly minLength?: number;
159
164
  readonly required?: boolean;
160
165
  readonly searchable?: boolean;
161
166
  readonly sortable?: boolean;
@@ -177,6 +182,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
177
182
  readonly type: "text";
178
183
  readonly description?: string;
179
184
  readonly maxLength?: number;
185
+ readonly minLength?: number;
180
186
  readonly required?: boolean;
181
187
  readonly searchable?: boolean;
182
188
  readonly sortable?: boolean;
@@ -209,14 +215,15 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
209
215
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
210
216
  readonly display?: "radio" | "dropdown";
211
217
  readonly optionTones?: {
212
- readonly active?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
213
- readonly destroyFailed?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
214
- readonly destroyRequested?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
215
- readonly destroyed?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
216
- readonly destroying?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
218
+ readonly active?: "bad" | "neutral" | "ok" | "warn" | undefined;
219
+ readonly destroyFailed?: "bad" | "neutral" | "ok" | "warn" | undefined;
220
+ readonly destroyRequested?: "bad" | "neutral" | "ok" | "warn" | undefined;
221
+ readonly destroyed?: "bad" | "neutral" | "ok" | "warn" | undefined;
222
+ readonly destroying?: "bad" | "neutral" | "ok" | "warn" | undefined;
217
223
  } | undefined;
218
224
  readonly optionsQuery?: string;
219
225
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
226
+ readonly optionsAvailabilityQuery?: string;
220
227
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
221
228
  required: true;
222
229
  };
@@ -239,6 +246,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
239
246
  readonly type: "text";
240
247
  readonly description?: string;
241
248
  readonly maxLength?: number;
249
+ readonly minLength?: number;
242
250
  readonly required?: boolean;
243
251
  readonly searchable?: boolean;
244
252
  readonly sortable?: boolean;
@@ -17,19 +17,17 @@ export const tenantEntity = createEntity({
17
17
  key: createTextField({
18
18
  required: true,
19
19
  maxLength: 50,
20
+ sortable: true,
20
21
  personal: false,
21
22
  reason: "technical_reference",
22
23
  }),
23
- // Workspace/company display name, not an individual's identity —
24
- // same class as the `company.legalName` example in the boot-validator's
25
- // own PII-heuristic doc-comment.
24
+ // Subject is the tenant record itself: tenant events live in the writer's (system) tenant stream, so `personal: "tenant"` would not resolve to this tenant.
26
25
  name: createTextField({
27
26
  required: true,
28
27
  maxLength: 200,
29
28
  searchable: true,
30
- sortable: true,
31
- personal: false,
32
- reason: "is_business_data",
29
+ personal: { of: "id" },
30
+ find: "none",
33
31
  }),
34
32
  isEnabled: createBooleanField({ default: true, filterable: true }),
35
33
  // Tenant-destroy lifecycle (tenant-lifecycle feature). Defaults keep
@@ -23,7 +23,7 @@ export const tenantListScreen = {
23
23
  entityId: "id",
24
24
  },
25
25
  ],
26
- defaultSort: { field: "name", dir: "asc" },
26
+ defaultSort: { field: "key", dir: "asc" },
27
27
  searchable: true,
28
28
  access: { roles: ["SystemAdmin"] },
29
29
  // Inert until an app navs it (see file header) — no nav area to resolve
@@ -174,7 +174,7 @@ export async function moveTransferGraph(args) {
174
174
  let pending = [
175
175
  { entityName: rootEntityName, rowIds: [rootRowId] },
176
176
  ];
177
- for (let round = 0; round < MAX_TRANSFER_DEPTH && pending.length > 0; round++) {
177
+ for (let round = 0; round <= MAX_TRANSFER_DEPTH && pending.length > 0; round++) {
178
178
  // Merged per type so an edge leaving a type runs once per round, not once
179
179
  // per edge that reached it.
180
180
  const discovered = new Map();
@@ -231,24 +231,17 @@ export async function moveTransferGraph(args) {
231
231
  }));
232
232
  }
233
233
  }
234
+ // The extra round past the limit is a probe: it only exists to find out
235
+ // whether rows still hang below the last allowed hop. Its moves are rolled
236
+ // back with the transaction when this throws.
237
+ const [strandedEntityName] = discovered.keys();
238
+ if (round === MAX_TRANSFER_DEPTH && strandedEntityName !== undefined) {
239
+ throw new UnprocessableError("transfer_graph_too_deep", {
240
+ i18nKey: "errors.tenantHandover.transferGraphTooDeep",
241
+ details: { entityName: strandedEntityName },
242
+ });
243
+ }
234
244
  pending = [...discovered].map(([entityName, rowIds]) => ({ entityName, rowIds }));
235
245
  }
236
- // The depth limit is a safety net, not a licence to move part of a graph: if
237
- // rounds ran out while rows remain whose type still leads somewhere, the
238
- // declaration reaches further than the mover does, and returning quietly
239
- // would leave exactly the orphaned rows #3088 is about.
240
- //
241
- // Rounds count hops of both edge kinds, while the boot validator measures
242
- // reference chains between transferable entities only. A graph that fills the
243
- // limit with reference hops and then adds a parentRef child therefore boots
244
- // clean and fails here instead — the residual the validator cannot see, and
245
- // the reason this check has to exist rather than trusting boot alone.
246
- const stranded = pending.find(({ entityName }) => (adjacency.get(entityName) ?? []).length > 0);
247
- if (stranded !== undefined) {
248
- throw new UnprocessableError("transfer_graph_too_deep", {
249
- i18nKey: "errors.tenantHandover.transferGraphTooDeep",
250
- details: { entityName: stranded.entityName },
251
- });
252
- }
253
246
  return movedCounts;
254
247
  }
@@ -85,6 +85,7 @@ export function createTenantLifecycleFeature() {
85
85
  now,
86
86
  log: (message) => ctx.log?.warn(message),
87
87
  fileProviderResolver: ctx._fileProviderResolver,
88
+ searchAdapter: ctx.searchAdapter,
88
89
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
89
90
  actor: ctx.systemUser.id,
90
91
  });
@@ -2,12 +2,14 @@
2
2
  import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { type EscapeHatchAuditSink, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
5
+ import type { SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
5
6
  export declare function runNextDestructionStage(args: {
6
7
  readonly db: DbRunner;
7
8
  readonly registry: Registry;
8
9
  readonly tenantId: TenantId;
9
10
  readonly log?: (message: string) => void;
10
11
  readonly fileProviderResolver?: FileProviderResolver;
12
+ readonly searchAdapter?: SearchAdapter;
11
13
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
12
14
  readonly actor?: string;
13
15
  }): Promise<{
@@ -23,6 +25,7 @@ export declare function runTenantDestructionSweep(args: {
23
25
  readonly now?: Temporal.Instant;
24
26
  readonly log?: (message: string) => void;
25
27
  readonly fileProviderResolver?: FileProviderResolver;
28
+ readonly searchAdapter?: SearchAdapter;
26
29
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
27
30
  readonly actor?: string;
28
31
  }): Promise<{
@@ -133,6 +133,7 @@ export async function runNextDestructionStage(args) {
133
133
  tenantId: args.tenantId,
134
134
  log: args.log,
135
135
  fileProviderResolver: args.fileProviderResolver,
136
+ searchAdapter: args.searchAdapter,
136
137
  escapeHatchAuditSink: args.escapeHatchAuditSink,
137
138
  actor: args.actor,
138
139
  };
@@ -215,6 +216,7 @@ export async function runTenantDestructionSweep(args) {
215
216
  tenantId: row.id,
216
217
  log: args.log,
217
218
  fileProviderResolver: args.fileProviderResolver,
219
+ searchAdapter: args.searchAdapter,
218
220
  escapeHatchAuditSink: args.escapeHatchAuditSink,
219
221
  actor: args.actor,
220
222
  });
@@ -1,6 +1,7 @@
1
1
  import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { type EscapeHatchAuditSink, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
4
+ import { type SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
4
5
  import type { TenantDestructionStageName } from "./constants.js";
5
6
  export type DestructionStageCtx = {
6
7
  readonly db: DbRunner;
@@ -8,6 +9,7 @@ export type DestructionStageCtx = {
8
9
  readonly tenantId: TenantId;
9
10
  readonly log?: (message: string) => void;
10
11
  readonly fileProviderResolver?: FileProviderResolver;
12
+ readonly searchAdapter?: SearchAdapter;
11
13
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
12
14
  readonly actor?: string;
13
15
  };
@@ -1,8 +1,9 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { configuredPiiSubjectKms } from "@cosmicdrift/kumiko-framework/crypto";
2
+ import { configuredPiiSubjectKms, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
3
3
  import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
4
4
  import { createSystemUser, EXT_EXTERNAL_RESOURCE, EXT_INFRA_RESOURCE, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, extensionUsageEscapeHatchReason, isTenantDataExtensionHooks, isTenantResourceExtensionHooks, } from "@cosmicdrift/kumiko-framework/engine";
5
5
  import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
6
+ import { purgeSearchDocumentsForSubject, } from "@cosmicdrift/kumiko-framework/search";
6
7
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
7
8
  import { tenantEntity, tenantMembershipEntity, tenantMembershipsTable, tenantTable, } from "../tenant/index.js";
8
9
  import { invalidateTenantLifecycleGate } from "./lifecycle-gate.js";
@@ -56,8 +57,10 @@ async function eraseSubjectKeys(ctx) {
56
57
  const memberships = await selectMany(ctx.db, tenantMembershipsTable, {
57
58
  tenantId: ctx.tenantId,
58
59
  });
60
+ const tenantRecordSubject = { kind: "record", entity: "tenant", id: ctx.tenantId };
59
61
  const subjects = [
60
62
  { kind: "tenant", tenantId: ctx.tenantId },
63
+ tenantRecordSubject,
61
64
  ...memberships.map((m) => ({ kind: "user", userId: m.userId })),
62
65
  ];
63
66
  for (const subject of subjects) {
@@ -66,6 +69,9 @@ async function eraseSubjectKeys(ctx) {
66
69
  eraseReason: "tenant-destroy stage subject-keys",
67
70
  });
68
71
  }
72
+ if (ctx.searchAdapter) {
73
+ await purgeSearchDocumentsForSubject(ctx.db, ctx.registry.features, ctx.searchAdapter, subjectIdToKey(tenantRecordSubject), tenantRecordSubject);
74
+ }
69
75
  }
70
76
  async function purgeTenantCache(ctx) {
71
77
  // ponytail: Redis SCAN+DEL is wired when ctx carries a redis client; until
@@ -3,6 +3,7 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
3
3
  readonly type: "text";
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
6
+ readonly minLength?: number;
6
7
  readonly required?: boolean;
7
8
  readonly searchable?: boolean;
8
9
  readonly sortable?: boolean;
@@ -24,6 +25,7 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
24
25
  readonly type: "text";
25
26
  readonly description?: string;
26
27
  readonly maxLength?: number;
28
+ readonly minLength?: number;
27
29
  readonly required?: boolean;
28
30
  readonly searchable?: boolean;
29
31
  readonly sortable?: boolean;
@@ -3,5 +3,6 @@ export { type AddOnDefinition, type AddOnMap, type ComposeAppInput, type Compose
3
3
  export { TIER_ENGINE_FEATURE, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, } from "./constants.js";
4
4
  export { tierAssignmentEntity } from "./entity.js";
5
5
  export { type CreateTierEngineOptions, createTierEngineFeature, tierEngineFeature, } from "./feature.js";
6
+ export { createTierOptionGate, type TierOptionAvailability, type TierOptionGate, type TierOptionGateDeps, type TierOptionSpec, } from "./tier-option-gate.js";
6
7
  export { createTierResolver, type TierResolver, type TierResolverDeps, } from "./tier-resolver.js";
7
8
  export { isTrialActive, type TrialPolicy } from "./trial.js";
@@ -11,5 +11,6 @@ export { composeApp, } from "./compose-app.js";
11
11
  export { TIER_ENGINE_FEATURE, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, } from "./constants.js";
12
12
  export { tierAssignmentEntity } from "./entity.js";
13
13
  export { createTierEngineFeature, tierEngineFeature, } from "./feature.js";
14
+ export { createTierOptionGate, } from "./tier-option-gate.js";
14
15
  export { createTierResolver, } from "./tier-resolver.js";
15
16
  export { isTrialActive } from "./trial.js";
@@ -0,0 +1,32 @@
1
+ import type { EntityTableMeta, SchemaTable, TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { type CapGuardContext, type CapLimitContext } from "../cap-counter/index.js";
5
+ export type TierOptionSpec<TCaps> = {
6
+ readonly field: string;
7
+ readonly options: readonly string[];
8
+ readonly isOptionAllowed: (option: string, caps: TCaps) => boolean;
9
+ readonly code: string;
10
+ readonly i18nKey: string;
11
+ /** Lets an update that resends the unchanged stored value of a no longer allowed
12
+ * option pass (edit forms submit the whole row). Without it the stored value is not looked up. */
13
+ readonly table?: SchemaTable | EntityTableMeta;
14
+ };
15
+ export type TierOptionAvailability = {
16
+ readonly value: string;
17
+ readonly disabled: boolean;
18
+ readonly hint?: string;
19
+ };
20
+ export type TierOptionGateDeps<TTier extends string, TCaps> = {
21
+ /** Ascending: the first tier that allows an option is the one named as required. */
22
+ readonly tierOrder: readonly TTier[];
23
+ readonly capsForTier: (tier: TTier, context: CapLimitContext) => TCaps | Promise<TCaps>;
24
+ readonly resolveTier: (db: TenantDb) => Promise<TTier>;
25
+ };
26
+ export type TierOptionGate<TTier extends string, TCaps> = {
27
+ readonly optionAvailability: (db: TenantDb, context: CapLimitContext, spec: Pick<TierOptionSpec<TCaps>, "options" | "isOptionAllowed">, hintFor: (requiredTier: TTier) => string) => Promise<readonly TierOptionAvailability[]>;
28
+ /** `recordId` is the id of the edited row; with `spec.table` it enables the unchanged-value pass. */
29
+ readonly checkTierOption: (ctx: CapGuardContext, value: unknown, spec: TierOptionSpec<TCaps>, recordId?: string) => Promise<WriteFailure | null>;
30
+ readonly withTierOptionGate: (handler: WriteHandlerDef, spec: TierOptionSpec<TCaps>) => WriteHandlerDef;
31
+ };
32
+ export declare function createTierOptionGate<TTier extends string, TCaps>(deps: TierOptionGateDeps<TTier, TCaps>): TierOptionGate<TTier, TCaps>;
@@ -0,0 +1,62 @@
1
+ // Tier-gated options of a select field: one spec drives the availability query the
2
+ // renderer shows (disabled option + "from <tier>" hint) and the write-path check that
3
+ // actually enforces it, so the two cannot drift apart.
4
+ import { UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
5
+ import { isPlainObject } from "@cosmicdrift/kumiko-framework/utils";
6
+ import { readPayloadField, } from "../cap-counter/index.js";
7
+ function limitContextOf(ctx) {
8
+ return ctx.config ? { config: ctx.config } : {};
9
+ }
10
+ function recordIdOf(payload) {
11
+ return isPlainObject(payload) && typeof payload["id"] === "string" ? payload["id"] : undefined;
12
+ }
13
+ export function createTierOptionGate(deps) {
14
+ async function capsPerTier(context) {
15
+ return Promise.all(deps.tierOrder.map(async (tier) => ({ tier, caps: await deps.capsForTier(tier, context) })));
16
+ }
17
+ async function optionAvailability(db, context, spec, hintFor) {
18
+ const currentCaps = await deps.capsForTier(await deps.resolveTier(db), context);
19
+ const tiers = await capsPerTier(context);
20
+ return spec.options.map((value) => {
21
+ if (spec.isOptionAllowed(value, currentCaps))
22
+ return { value, disabled: false };
23
+ const required = tiers.find(({ caps }) => spec.isOptionAllowed(value, caps));
24
+ return required === undefined
25
+ ? { value, disabled: true }
26
+ : { value, disabled: true, hint: hintFor(required.tier) };
27
+ });
28
+ }
29
+ async function storedValueMatches(ctx, spec, value, recordId) {
30
+ if (spec.table === undefined || recordId === undefined)
31
+ return false;
32
+ const stored = await ctx.db.fetchOne(spec.table, { id: recordId });
33
+ return stored?.[spec.field] === value;
34
+ }
35
+ async function checkTierOption(ctx, value, spec, recordId) {
36
+ // skip: an absent or non-string value is the schema's business, not a tier violation.
37
+ if (typeof value !== "string")
38
+ return null;
39
+ const context = limitContextOf(ctx);
40
+ const currentCaps = await deps.capsForTier(await deps.resolveTier(ctx.db), context);
41
+ if (spec.isOptionAllowed(value, currentCaps))
42
+ return null;
43
+ if (await storedValueMatches(ctx, spec, value, recordId))
44
+ return null;
45
+ const required = (await capsPerTier(context)).find(({ caps }) => spec.isOptionAllowed(value, caps));
46
+ return writeFailure(new UnprocessableError(spec.code, {
47
+ i18nKey: spec.i18nKey,
48
+ details: { field: spec.field, value, requiredTier: required?.tier },
49
+ }));
50
+ }
51
+ function withTierOptionGate(handler, spec) {
52
+ return {
53
+ ...handler,
54
+ handler: async (event, ctx) => {
55
+ const value = readPayloadField(event.payload, spec.field);
56
+ const failure = await checkTierOption(ctx, value, spec, recordIdOf(event.payload));
57
+ return failure ?? handler.handler(event, ctx);
58
+ },
59
+ };
60
+ }
61
+ return { optionAvailability, checkTierOption, withTierOptionGate };
62
+ }
@@ -11,6 +11,7 @@ export declare function createTierResolver<TTier extends string, TCaps>(deps: Ti
11
11
  readonly type: "text";
12
12
  readonly description?: string;
13
13
  readonly maxLength?: number;
14
+ readonly minLength?: number;
14
15
  readonly required?: boolean;
15
16
  readonly searchable?: boolean;
16
17
  readonly sortable?: boolean;
@@ -32,6 +33,7 @@ export declare function createTierResolver<TTier extends string, TCaps>(deps: Ti
32
33
  readonly type: "text";
33
34
  readonly description?: string;
34
35
  readonly maxLength?: number;
36
+ readonly minLength?: number;
35
37
  readonly required?: boolean;
36
38
  readonly searchable?: boolean;
37
39
  readonly sortable?: boolean;
@@ -2,7 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { access, defineEntityListHandler, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
4
4
  // kumiko-lint-ignore cross-feature-import SystemAdmin user-list joins memberships for tenants column
5
- import { tenantMembershipsTable, tenantTable } from "../../tenant/index.js";
5
+ import { decryptTenantNames, tenantMembershipsTable, tenantTable } from "../../tenant/index.js";
6
6
  import { userEntity } from "../schema/user.js";
7
7
  const baseList = defineEntityListHandler("user", userEntity, {
8
8
  access: { roles: access.systemAdmin },
@@ -59,7 +59,7 @@ export async function attachTenantLabels(rows, db) {
59
59
  ...new Set(memberships.map((m) => String(m.tenantId ?? "")).filter((id) => id !== "")),
60
60
  ];
61
61
  const tenants = tenantIds.length > 0
62
- ? await selectMany(db, tenantTable, { id: { in: tenantIds } })
62
+ ? await decryptTenantNames(await selectMany(db, tenantTable, { id: { in: tenantIds } }), "user:list")
63
63
  : [];
64
64
  const byUser = labelsByUserId(memberships, tenantLabelById(tenants));
65
65
  return rows.map((row) => {
@@ -32,6 +32,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
32
32
  readonly type: "text";
33
33
  readonly description?: string;
34
34
  readonly maxLength?: number;
35
+ readonly minLength?: number;
35
36
  readonly required?: boolean;
36
37
  readonly searchable?: boolean;
37
38
  readonly sortable?: boolean;
@@ -53,6 +54,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
53
54
  readonly type: "text";
54
55
  readonly description?: string;
55
56
  readonly maxLength?: number;
57
+ readonly minLength?: number;
56
58
  readonly required?: boolean;
57
59
  readonly searchable?: boolean;
58
60
  readonly sortable?: boolean;
@@ -74,6 +76,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
74
76
  readonly type: "text";
75
77
  readonly description?: string;
76
78
  readonly maxLength?: number;
79
+ readonly minLength?: number;
77
80
  readonly required?: boolean;
78
81
  readonly searchable?: boolean;
79
82
  readonly sortable?: boolean;
@@ -95,6 +98,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
95
98
  readonly type: "text";
96
99
  readonly description?: string;
97
100
  readonly maxLength?: number;
101
+ readonly minLength?: number;
98
102
  readonly required?: boolean;
99
103
  readonly searchable?: boolean;
100
104
  readonly sortable?: boolean;
@@ -125,6 +129,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
125
129
  readonly type: "text";
126
130
  readonly description?: string;
127
131
  readonly maxLength?: number;
132
+ readonly minLength?: number;
128
133
  readonly required?: boolean;
129
134
  readonly searchable?: boolean;
130
135
  readonly sortable?: boolean;
@@ -158,13 +163,14 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
158
163
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
159
164
  readonly display?: "radio" | "dropdown";
160
165
  readonly optionTones?: {
161
- readonly active?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
162
- readonly deleted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
163
- readonly deletionRequested?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
164
- readonly restricted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
166
+ readonly active?: "bad" | "neutral" | "ok" | "warn" | undefined;
167
+ readonly deleted?: "bad" | "neutral" | "ok" | "warn" | undefined;
168
+ readonly deletionRequested?: "bad" | "neutral" | "ok" | "warn" | undefined;
169
+ readonly restricted?: "bad" | "neutral" | "ok" | "warn" | undefined;
165
170
  } | undefined;
166
171
  readonly optionsQuery?: string;
167
172
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
173
+ readonly optionsAvailabilityQuery?: string;
168
174
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
169
175
  required: true;
170
176
  };
@@ -187,6 +193,7 @@ export declare const userEntity: import("@cosmicdrift/kumiko-types/fields").Enti
187
193
  readonly type: "text";
188
194
  readonly description?: string;
189
195
  readonly maxLength?: number;
196
+ readonly minLength?: number;
190
197
  readonly required?: boolean;
191
198
  readonly searchable?: boolean;
192
199
  readonly sortable?: boolean;
@@ -210,6 +217,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
210
217
  readonly type: "text";
211
218
  readonly description?: string;
212
219
  readonly maxLength?: number;
220
+ readonly minLength?: number;
213
221
  readonly required?: boolean;
214
222
  readonly searchable?: boolean;
215
223
  readonly sortable?: boolean;
@@ -231,6 +239,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
231
239
  readonly type: "text";
232
240
  readonly description?: string;
233
241
  readonly maxLength?: number;
242
+ readonly minLength?: number;
234
243
  readonly required?: boolean;
235
244
  readonly searchable?: boolean;
236
245
  readonly sortable?: boolean;
@@ -252,6 +261,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
252
261
  readonly type: "text";
253
262
  readonly description?: string;
254
263
  readonly maxLength?: number;
264
+ readonly minLength?: number;
255
265
  readonly required?: boolean;
256
266
  readonly searchable?: boolean;
257
267
  readonly sortable?: boolean;
@@ -273,6 +283,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
273
283
  readonly type: "text";
274
284
  readonly description?: string;
275
285
  readonly maxLength?: number;
286
+ readonly minLength?: number;
276
287
  readonly required?: boolean;
277
288
  readonly searchable?: boolean;
278
289
  readonly sortable?: boolean;
@@ -303,6 +314,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
303
314
  readonly type: "text";
304
315
  readonly description?: string;
305
316
  readonly maxLength?: number;
317
+ readonly minLength?: number;
306
318
  readonly required?: boolean;
307
319
  readonly searchable?: boolean;
308
320
  readonly sortable?: boolean;
@@ -336,13 +348,14 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
336
348
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
337
349
  readonly display?: "radio" | "dropdown";
338
350
  readonly optionTones?: {
339
- readonly active?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
340
- readonly deleted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
341
- readonly deletionRequested?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
342
- readonly restricted?: import("@cosmicdrift/kumiko-types/fields").SelectOptionTone | undefined;
351
+ readonly active?: "bad" | "neutral" | "ok" | "warn" | undefined;
352
+ readonly deleted?: "bad" | "neutral" | "ok" | "warn" | undefined;
353
+ readonly deletionRequested?: "bad" | "neutral" | "ok" | "warn" | undefined;
354
+ readonly restricted?: "bad" | "neutral" | "ok" | "warn" | undefined;
343
355
  } | undefined;
344
356
  readonly optionsQuery?: string;
345
357
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
358
+ readonly optionsAvailabilityQuery?: string;
346
359
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
347
360
  required: true;
348
361
  };
@@ -365,6 +378,7 @@ export declare const userTable: import("@cosmicdrift/kumiko-framework/db").Entit
365
378
  readonly type: "text";
366
379
  readonly description?: string;
367
380
  readonly maxLength?: number;
381
+ readonly minLength?: number;
368
382
  readonly required?: boolean;
369
383
  readonly searchable?: boolean;
370
384
  readonly sortable?: boolean;
@@ -20,6 +20,11 @@ export function validateGdprHookCompleteness(features) {
20
20
  continue;
21
21
  const hasExport = typeof usage.options?.["export"] === "function";
22
22
  const hasDelete = typeof usage.options?.["delete"] === "function";
23
+ // The compile-time "at least one hook" type is bypassed by string-typed
24
+ // extension names and casts; without this an empty bag registers nothing.
25
+ if (!hasExport && !hasDelete) {
26
+ throw new Error(`[kumiko:boot] Feature "${feature.name}" registers EXT_USER_DATA for entity "${usage.entityName}" with neither an export nor a delete hook — the entity silently stays out of the Art.15/17/20 pipeline. Register at least one hook.`);
27
+ }
23
28
  if (hasExport && !hasDelete) {
24
29
  throw new Error(`[kumiko:boot] Feature "${feature.name}" exports entity "${usage.entityName}" via EXT_USER_DATA but registers no delete hook — data is included in Art.20 exports but never erased on forget (Art.17 violation). Add a delete hook. If erasure is intentionally handled elsewhere (e.g. crypto-shredding key-erase, parent cascade), register a no-op delete: async () => {} with a comment explaining why.`);
25
30
  }
@@ -28,7 +28,7 @@ async function readPendingDeletionRequestId(ctx, userId) {
28
28
  }
29
29
  // Anonymous apex flow step 2: verify-link target. Verifies the HMAC token,
30
30
  // extracts the userId, and flips the grace period through the shared logic —
31
- // in ONE atomic step with the anchor spend (#3024): commitDeletion carries
31
+ // in ONE atomic step with the anchor spend: commitDeletion carries
32
32
  // the actual grace-period transition, so a crash after the spend can no
33
33
  // longer lose the write step.
34
34
  //
@@ -38,7 +38,7 @@ async function readPendingDeletionRequestId(ctx, userId) {
38
38
  // a cancel-deletion (status → Active, pendingDeletionRequestId → null), a
39
39
  // replayed token fails against the nulled/renewed requestId — no re-arm.
40
40
  //
41
- // Concurrency (#3024): two simultaneous confirms of the same token both read
41
+ // Concurrency: two simultaneous confirms of the same token both read
42
42
  // the same requestId and both verify the HMAC — commitDeletion spends the
43
43
  // anchor AND writes the transition atomically (expect: status===Active &&
44
44
  // pendingDeletionRequestId===requestId), so exactly one wins. The loser gets