@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.
- package/dist/agent-tools/agent-manifest.js +8 -2
- package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
- package/dist/agent-tools/manifest-deny-qns.js +17 -0
- package/dist/agent-tools/tool-catalog.d.ts +2 -1
- package/dist/agent-tools/tool-catalog.js +4 -1
- package/dist/audit/handlers/list.query.js +13 -8
- package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -3
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +2 -2
- package/dist/billing-foundation/checkout-core.d.ts +7 -0
- package/dist/billing-foundation/checkout-core.js +7 -2
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/book-cap-usage.d.ts +5 -1
- package/dist/cap-counter/book-cap-usage.js +65 -44
- package/dist/cap-counter/cap-reservation.d.ts +12 -0
- package/dist/cap-counter/cap-reservation.js +135 -0
- package/dist/cap-counter/constants.d.ts +3 -0
- package/dist/cap-counter/constants.js +6 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-counter/feature.js +7 -2
- package/dist/cap-counter/handlers/increment-rolling.write.d.ts +5 -0
- package/dist/cap-counter/handlers/increment-rolling.write.js +4 -0
- package/dist/cap-counter/index.d.ts +4 -2
- package/dist/cap-counter/index.js +3 -1
- package/dist/cap-counter/payload-field.d.ts +1 -0
- package/dist/cap-counter/payload-field.js +11 -0
- package/dist/cap-counter/reservation-chain.d.ts +2 -0
- package/dist/cap-counter/reservation-chain.js +44 -0
- package/dist/cap-counter/stock-cap-guard.d.ts +3 -0
- package/dist/cap-counter/tables.d.ts +3 -0
- package/dist/cap-counter/tables.js +36 -0
- package/dist/cap-counter/value-cap-guard.d.ts +18 -0
- package/dist/cap-counter/value-cap-guard.js +33 -0
- package/dist/cap-counter/with-cap-enforcement.d.ts +5 -7
- package/dist/cap-counter/with-cap-enforcement.js +35 -51
- package/dist/cap-overview/access.js +2 -9
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
- package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
- package/dist/channel-discord/feature.js +1 -0
- package/dist/channel-slack/feature.js +1 -0
- package/dist/channel-teams/feature.js +1 -0
- package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
- package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
- package/dist/config/handlers/values.query.d.ts +3 -1
- package/dist/config/handlers/values.query.js +7 -2
- package/dist/config/table.d.ts +6 -0
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/attempt-log-retention.d.ts +9 -0
- package/dist/delivery/attempt-log-retention.js +23 -0
- package/dist/delivery/attempt-log.d.ts +1 -1
- package/dist/delivery/attempt-log.js +21 -2
- package/dist/delivery/constants.d.ts +1 -1
- package/dist/delivery/constants.js +1 -1
- package/dist/delivery/events.d.ts +1 -0
- package/dist/delivery/events.js +2 -0
- package/dist/delivery/feature.d.ts +1 -0
- package/dist/delivery/feature.js +39 -3
- package/dist/delivery/index.d.ts +2 -1
- package/dist/delivery/index.js +2 -1
- package/dist/delivery/mask-recipient-address.d.ts +1 -0
- package/dist/delivery/mask-recipient-address.js +41 -0
- package/dist/delivery/public-names.d.ts +2 -0
- package/dist/delivery/public-names.js +4 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/types.d.ts +1 -0
- package/dist/delivery/types.js +6 -2
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/files-tenant-data/hooks.js +3 -2
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/feature.js +2 -10
- package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
- package/dist/form-draft/db/queries/cleanup.js +4 -3
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
- package/dist/form-draft/handlers/cleanup.job.js +11 -5
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/jobs/handlers/list.query.d.ts +2 -1
- package/dist/jobs/handlers/list.query.js +39 -14
- package/dist/jobs/i18n.js +1 -0
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/ledger/entity.d.ts +24 -8
- package/dist/ledger/entity.js +4 -0
- package/dist/ledger/schemas.js +8 -2
- package/dist/managed-pages/feature.js +28 -10
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/feature.js +2 -10
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +14 -3
- package/dist/renderer-foundation/types.d.ts +1 -0
- package/dist/renderer-simple/feature.js +4 -6
- package/dist/renderer-simple/index.d.ts +1 -0
- package/dist/renderer-simple/mail-blocks.d.ts +21 -0
- package/dist/renderer-simple/mail-blocks.js +107 -0
- package/dist/renderer-simple/simple-renderer.js +21 -3
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/feature.js +28 -6
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
- package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
- package/dist/shared/index.d.ts +2 -1
- package/dist/shared/index.js +2 -1
- package/dist/shared/rate-limited-text-response.d.ts +2 -0
- package/dist/shared/rate-limited-text-response.js +9 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/feature.js +2 -10
- package/dist/template-resolver/table.d.ts +48 -30
- package/dist/template-resolver/user-content-table.d.ts +34 -20
- package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
- package/dist/tenant/decrypt-tenant-names.js +10 -0
- package/dist/tenant/handlers/me.query.js +5 -1
- package/dist/tenant/handlers/memberships.query.js +3 -2
- package/dist/tenant/handlers/tenant-directory.query.js +9 -3
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +16 -8
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +18 -10
- package/dist/tenant/schema/tenant.js +4 -6
- package/dist/tenant/screens.js +1 -1
- package/dist/tenant-handover/move-entity-graph.js +11 -18
- package/dist/tenant-lifecycle/feature.js +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +3 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -0
- package/dist/tenant-lifecycle/stages.d.ts +2 -0
- package/dist/tenant-lifecycle/stages.js +7 -1
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/index.d.ts +1 -0
- package/dist/tier-engine/index.js +1 -0
- package/dist/tier-engine/tier-option-gate.d.ts +32 -0
- package/dist/tier-engine/tier-option-gate.js +62 -0
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/handlers/list.query.js +2 -2
- package/dist/user/schema/user.d.ts +22 -8
- package/dist/user-data-rights/boot-checks.js +5 -0
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +16 -8
- package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +25 -6
- package/dist/waitlist/entity.d.ts +22 -6
- package/dist/workflow-runner/event-trigger.js +6 -3
- package/dist/workflow-runner/handlers/resume-run.write.js +3 -0
- package/package.json +10 -10
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +6 -0
- package/src/cap-counter/changes.json +28 -0
- package/src/delivery/changes.json +12 -0
- package/src/derivatives-sharp/changes.json +6 -0
- package/src/form-draft/changes.json +7 -1
- package/src/legal-pages/changes.json +6 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-simple/changes.json +6 -0
- package/src/seo/changes.json +5 -0
- package/src/tenant/changes.json +7 -0
- package/src/tenant-handover/changes.json +6 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/workflow-runner/changes.json +5 -0
- package/dist/shared/has-where-rule.d.ts +0 -2
- 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?:
|
|
62
|
-
readonly destroyFailed?:
|
|
63
|
-
readonly destroyRequested?:
|
|
64
|
-
readonly destroyed?:
|
|
65
|
-
readonly destroying?:
|
|
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?:
|
|
213
|
-
readonly destroyFailed?:
|
|
214
|
-
readonly destroyRequested?:
|
|
215
|
-
readonly destroyed?:
|
|
216
|
-
readonly destroying?:
|
|
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
|
-
//
|
|
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
|
-
|
|
31
|
-
|
|
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
|
package/dist/tenant/screens.js
CHANGED
|
@@ -23,7 +23,7 @@ export const tenantListScreen = {
|
|
|
23
23
|
entityId: "id",
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
|
-
defaultSort: { field: "
|
|
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
|
|
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?:
|
|
162
|
-
readonly deleted?:
|
|
163
|
-
readonly deletionRequested?:
|
|
164
|
-
readonly restricted?:
|
|
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?:
|
|
340
|
-
readonly deleted?:
|
|
341
|
-
readonly deletionRequested?:
|
|
342
|
-
readonly restricted?:
|
|
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
|
|
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
|
|
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
|