@cosmicdrift/kumiko-framework 0.371.1 → 0.373.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/AGENTS.md +4 -1
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/embedded-entity-form.md +1 -1
- package/agents/examples/pdf-audit-export.md +1 -1
- package/agents/examples/rate-limiting.md +65 -1
- package/agents/examples/user-data-rights.md +176 -0
- package/dist/api/jwt.js +2 -1
- package/dist/api/observability-middleware.d.ts +1 -2
- package/dist/api/observability-middleware.js +9 -8
- package/dist/api/pii-leak-guard.js +2 -1
- package/dist/api/route-label.d.ts +3 -0
- package/dist/api/route-label.js +17 -0
- package/dist/api/server.d.ts +1 -2
- package/dist/api/server.js +3 -5
- package/dist/config/index.d.ts +5 -0
- package/dist/db/event-store-executor.d.ts +2 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/db/located-timestamp.d.ts +7 -0
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +8 -2
- package/dist/db/table-builder.d.ts +45 -11
- package/dist/db/tenant-db.js +3 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +47 -16
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/factories.d.ts +2 -1
- package/dist/engine/factories.js +2 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +8 -8
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +150 -6
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/membership-roles.d.ts +1 -0
- package/dist/engine/membership-roles.js +8 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +30 -2
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/types/index.js +1 -1
- package/dist/entrypoint/index.d.ts +1 -0
- package/dist/entrypoint/index.js +0 -2
- package/dist/files/assert-file-refs-belong-to-tenant.js +27 -1
- package/dist/files/bind-pending-file-refs.d.ts +31 -1
- package/dist/files/bind-pending-file-refs.js +80 -6
- package/dist/files/create-file-ref-from-bytes.js +12 -6
- package/dist/files/create-file-ref.d.ts +20 -2
- package/dist/files/create-file-ref.js +10 -1
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +25 -0
- package/dist/files/file-handle.d.ts +4 -1
- package/dist/files/file-handle.js +30 -4
- package/dist/files/file-ref-entity.d.ts +7 -7
- package/dist/files/file-ref-table.d.ts +7 -7
- package/dist/files/file-routes.js +6 -1
- package/dist/files/index.d.ts +5 -1
- package/dist/files/index.js +4 -1
- package/dist/files/load-save-file-refs.js +3 -1
- package/dist/files/provider-resolver.js +3 -1
- package/dist/files/sweep-orphaned-uploads.d.ts +16 -0
- package/dist/files/sweep-orphaned-uploads.js +53 -0
- package/dist/files/upload-ledger.d.ts +10 -0
- package/dist/files/upload-ledger.js +47 -0
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/request-locale.d.ts +15 -4
- package/dist/i18n/request-locale.js +26 -9
- package/dist/jobs/job-runner.js +2 -4
- package/dist/pipeline/dispatch-shared.js +57 -39
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/rate-limit/index.d.ts +2 -1
- package/dist/rate-limit/index.js +1 -0
- package/dist/rate-limit/resolver.d.ts +1 -1
- package/dist/rate-limit/resolver.js +104 -2
- package/dist/rate-limit/unlimited-resolver.d.ts +2 -0
- package/dist/rate-limit/unlimited-resolver.js +22 -0
- package/dist/schema-cli.js +46 -5
- package/package.json +5 -5
- package/src/changes.json +55 -0
- package/src/scripts/codemod/README.md +22 -0
- package/src/scripts/codemod/remove-engine-payload-casts.ts +318 -0
|
@@ -3,6 +3,8 @@ import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
|
3
3
|
import type { TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
4
4
|
import type { EntityDefinition, EntityRelations, FieldDefinition, FieldsMap } from "../engine/types/index.ts";
|
|
5
5
|
import type { Table } from "./event-store-executor-context.ts";
|
|
6
|
+
import type { LocatedTimestampRead } from "./located-timestamp.ts";
|
|
7
|
+
import type { MoneyRead } from "./money.ts";
|
|
6
8
|
export { toSnakeCase } from "../utils/case.ts";
|
|
7
9
|
import { type ColumnBuilder, type ColumnHandle, type SchemaTable, type TableColumns } from "./dialect.ts";
|
|
8
10
|
export declare const SOFT_DELETE_LIVE_ROW_PREDICATE = "\"is_deleted\" = false";
|
|
@@ -10,25 +12,23 @@ export declare function physicalColumnName(table: Table, field: string): string;
|
|
|
10
12
|
export type AnyPgColumn = ColumnHandle;
|
|
11
13
|
export declare const READ_MODEL_PREFIX = "read_";
|
|
12
14
|
export declare function toTableName(entityName: string): string;
|
|
13
|
-
type Col<
|
|
15
|
+
type Col<T> = ColumnHandle & {
|
|
14
16
|
readonly __notNull: true;
|
|
17
|
+
readonly __value?: T;
|
|
15
18
|
};
|
|
16
|
-
type NullCol<
|
|
19
|
+
type NullCol<T> = ColumnHandle & {
|
|
17
20
|
readonly __notNull: false;
|
|
21
|
+
readonly __value?: T;
|
|
18
22
|
};
|
|
19
|
-
type ColumnsForField<K extends string, F extends FieldDefinition> = F
|
|
23
|
+
type ColumnsForField<K extends string, F extends FieldDefinition> = ValueColumnsForField<K, F> & BlindIndexColumnForField<K, F>;
|
|
24
|
+
type BlindIndexColumnForField<K extends string, F extends FieldDefinition> = F extends {
|
|
20
25
|
type: "text";
|
|
21
26
|
lookupable: true;
|
|
22
|
-
} ? (F extends {
|
|
23
|
-
required: true;
|
|
24
27
|
} ? {
|
|
25
|
-
readonly [P in K]: Col<string>;
|
|
26
|
-
} : {
|
|
27
|
-
readonly [P in K]: NullCol<string>;
|
|
28
|
-
}) & {
|
|
29
28
|
readonly [P in `${K}Bidx`]: NullCol<string>;
|
|
30
|
-
} :
|
|
31
|
-
|
|
29
|
+
} : unknown;
|
|
30
|
+
type ValueColumnsForField<K extends string, F extends FieldDefinition> = F extends {
|
|
31
|
+
type: "text" | "longText" | "select" | "tz";
|
|
32
32
|
} ? F extends {
|
|
33
33
|
required: true;
|
|
34
34
|
} ? {
|
|
@@ -160,6 +160,40 @@ type SoftDeleteColumnsType = {
|
|
|
160
160
|
readonly deletedAt: NullCol<Temporal.Instant>;
|
|
161
161
|
readonly deletedById: NullCol<string>;
|
|
162
162
|
};
|
|
163
|
+
type ColumnValue<C> = C extends {
|
|
164
|
+
readonly __notNull: true;
|
|
165
|
+
readonly __value?: infer T;
|
|
166
|
+
} ? T : C extends {
|
|
167
|
+
readonly __notNull: false;
|
|
168
|
+
readonly __value?: infer T;
|
|
169
|
+
} ? T | null : unknown;
|
|
170
|
+
type RowOfColumns<C> = {
|
|
171
|
+
readonly [K in keyof C]: ColumnValue<C[K]>;
|
|
172
|
+
};
|
|
173
|
+
type CompoundRowField<K extends string, V, F> = F extends {
|
|
174
|
+
required: true;
|
|
175
|
+
} ? {
|
|
176
|
+
readonly [P in K]: V;
|
|
177
|
+
} : {
|
|
178
|
+
readonly [P in K]?: V;
|
|
179
|
+
};
|
|
180
|
+
type RowForField<K extends string, F extends FieldDefinition> = F extends {
|
|
181
|
+
type: "money";
|
|
182
|
+
} ? CompoundRowField<K, MoneyRead, F> : F extends {
|
|
183
|
+
type: "locatedTimestamp";
|
|
184
|
+
} ? CompoundRowField<K, LocatedTimestampRead, F> : [ValueColumnsForField<K, F>] extends [never] ? {
|
|
185
|
+
readonly [P in K]: unknown;
|
|
186
|
+
} : RowOfColumns<ValueColumnsForField<K, F>>;
|
|
187
|
+
type RowForFields<F extends FieldsMap> = UnionToIntersection<{
|
|
188
|
+
[K in keyof F & string]: RowForField<K, F[K]>;
|
|
189
|
+
}[keyof F & string]>;
|
|
190
|
+
type SoftDeleteRow<E extends EntityDefinition> = E["softDelete"] extends true ? RowOfColumns<SoftDeleteColumnsType> : [E["softDelete"]] extends [false | undefined] ? unknown : Partial<RowOfColumns<SoftDeleteColumnsType>>;
|
|
191
|
+
type Flatten<T> = {
|
|
192
|
+
[K in keyof T]: T[K];
|
|
193
|
+
} & {};
|
|
194
|
+
/** The row `executor.list()` / `executor.detail()` return for entity `E`. A wide
|
|
195
|
+
* `EntityDefinition` (fields without literal keys) yields `Record<string, unknown>`. */
|
|
196
|
+
export type EntityRow<E extends EntityDefinition = EntityDefinition> = string extends keyof E["fields"] ? Record<string, unknown> : Flatten<RowOfColumns<BaseColumnsType<E>> & SoftDeleteRow<E> & RowForFields<E["fields"]>>;
|
|
163
197
|
export type { ExecutorOnly, NotExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
164
198
|
export type { EntityTenancy, TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
165
199
|
type EntityTableTenancy<E extends EntityDefinition> = [NonNullable<E["tenancy"]>] extends [never] ? "tenant" : [NonNullable<E["tenancy"]>] extends ["global"] ? "global" : "tenant";
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -504,7 +504,9 @@ function buildTenantDb(options) {
|
|
|
504
504
|
return Promise.reject(new Error("db.global().deleteMany without where would mass-delete every tenant's rows. Pass at least one where condition."));
|
|
505
505
|
}
|
|
506
506
|
report("global-write", globalWriteReason());
|
|
507
|
-
return withDbSpan("delete", table, async () =>
|
|
507
|
+
return withDbSpan("delete", table, async () => {
|
|
508
|
+
await bunDeleteMany(db, table, where);
|
|
509
|
+
});
|
|
508
510
|
},
|
|
509
511
|
// @cast-boundary phantom-brand — GlobalWrites is only present in the type when TTable is not ExecutorOnly; the object above always carries the methods.
|
|
510
512
|
};
|
|
@@ -10,6 +10,7 @@ export declare function validateAdditionalRateLimits(featureName: string, handle
|
|
|
10
10
|
export declare function validatePerCommandTransaction(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
11
11
|
export declare function validateTenantlessAnonymous(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
12
12
|
export declare function validateRateLimitDisabledReason(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
13
|
+
export declare function validateRateLimitBucketList(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
13
14
|
export declare function validateNoAllRoleInHandlerAccess(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>): void;
|
|
14
15
|
export declare function validateAnonymousRateLimit(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
15
16
|
export declare function validateMultiStreamProjections(feature: FeatureDefinition): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SELECT_OPTION_TONES } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
-
import { isRateLimitDisabled } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { declaredRateLimitOptions, isRateLimitDisabled } from "@cosmicdrift/kumiko-types/handlers";
|
|
3
3
|
import { ZodObject } from "zod";
|
|
4
4
|
import { VARIANT_NAME_PATTERN } from "../../derivatives/variant-key.js";
|
|
5
5
|
import { access as accessPresets } from "../config-helpers.js";
|
|
@@ -133,6 +133,7 @@ export function validateHandlerAccess(feature) {
|
|
|
133
133
|
`Set { roles: [...] } for role-based access, or { openToAll: { reason: "..." } } for any authenticated user.`);
|
|
134
134
|
}
|
|
135
135
|
validateNoAllRoleInHandlerAccess(feature.name, kind, name, handler.access);
|
|
136
|
+
validateRateLimitBucketList(feature.name, kind, name, handler.rateLimit);
|
|
136
137
|
validateAnonymousRateLimit(feature.name, kind, name, handler.access, handler.rateLimit);
|
|
137
138
|
validateRateLimitDisabledReason(feature.name, kind, name, handler.rateLimit);
|
|
138
139
|
}
|
|
@@ -158,8 +159,17 @@ export function validateAdditionalRateLimits(featureName, handlerName, handler)
|
|
|
158
159
|
}
|
|
159
160
|
validateAnonymousHasIpBucket(where, handler);
|
|
160
161
|
const shape = handler.schema instanceof ZodObject ? handler.schema.shape : undefined;
|
|
162
|
+
const seenFields = new Set();
|
|
161
163
|
for (const entry of additionalRateLimits) {
|
|
162
164
|
validatePayloadBucket(where, shape, entry);
|
|
165
|
+
const field = entry.per.payloadField;
|
|
166
|
+
// Two entries on one field hash the same value into the same Redis key, so enforceAll
|
|
167
|
+
// would deduct only one of the costs.
|
|
168
|
+
if (seenFields.has(field)) {
|
|
169
|
+
throw new Error(`${where} with payloadField "${field}" more than once — both entries would share one ` +
|
|
170
|
+
`Redis bucket per value. Keep one entry per payloadField.`);
|
|
171
|
+
}
|
|
172
|
+
seenFields.add(field);
|
|
163
173
|
}
|
|
164
174
|
}
|
|
165
175
|
function validateAnonymousHasIpBucket(where, handler) {
|
|
@@ -168,9 +178,7 @@ function validateAnonymousHasIpBucket(where, handler) {
|
|
|
168
178
|
// skip: only anonymous callers need an ip bucket
|
|
169
179
|
if (!allowsAnonymous)
|
|
170
180
|
return;
|
|
171
|
-
const hasIpBucket = rateLimit
|
|
172
|
-
!isRateLimitDisabled(rateLimit) &&
|
|
173
|
-
IP_BUCKETED_RATE_LIMIT_PER.has(rateLimit.per);
|
|
181
|
+
const hasIpBucket = declaredRateLimitOptions(rateLimit).some((option) => IP_BUCKETED_RATE_LIMIT_PER.has(option.per));
|
|
174
182
|
if (!hasIpBucket) {
|
|
175
183
|
throw new Error(`${where} and allows anonymous callers, but has no ip-keyed rateLimit ` +
|
|
176
184
|
`(per: "ip" or "ip+handler", not disabled). Payload buckets only complement the IP bucket. Set rateLimit: { per: "ip", ... } on the handler.`);
|
|
@@ -217,13 +225,16 @@ export function validateTenantlessAnonymous(featureName, handlerName, handler) {
|
|
|
217
225
|
throw new Error(`${where} but its access is not exactly { roles: ["anonymous"] } — a tenantless request ` +
|
|
218
226
|
`carries no principal beyond the anonymous visitor. Set access to { roles: ["anonymous"] } or remove tenantlessAnonymous.`);
|
|
219
227
|
}
|
|
220
|
-
|
|
228
|
+
const options = declaredRateLimitOptions(handler.rateLimit);
|
|
229
|
+
if (options.length === 0) {
|
|
221
230
|
throw new Error(`${where} without a rateLimit — a handler reachable on hosts that resolve no tenant ` +
|
|
222
231
|
`needs a real upper bound (rateLimit: { per: "ip", ... }); { disabled: true } does not count.`);
|
|
223
232
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
for (const { per } of options) {
|
|
234
|
+
if (IP_SCOPED_RATE_LIMIT_PER.includes(per))
|
|
235
|
+
continue;
|
|
236
|
+
throw new Error(`${where} with rateLimit.per "${per}" — every anonymous visitor shares the same ` +
|
|
237
|
+
`tenant/user bucket, so one caller could exhaust it for all. Use ${IP_SCOPED_RATE_LIMIT_PER.map((allowed) => `"${allowed}"`).join(" or ")}.`);
|
|
227
238
|
}
|
|
228
239
|
}
|
|
229
240
|
export function validateRateLimitDisabledReason(featureName, kind, handlerName, rateLimit) {
|
|
@@ -236,6 +247,23 @@ export function validateRateLimitDisabledReason(featureName, kind, handlerName,
|
|
|
236
247
|
`be rate-limited.`);
|
|
237
248
|
}
|
|
238
249
|
}
|
|
250
|
+
export function validateRateLimitBucketList(featureName, kind, handlerName, rateLimit) {
|
|
251
|
+
// skip: a single option or a disabled declaration has no bucket list to check
|
|
252
|
+
if (!Array.isArray(rateLimit))
|
|
253
|
+
return;
|
|
254
|
+
const where = `${kind} handler "${featureName}:${kind}:${handlerName}"`;
|
|
255
|
+
if (rateLimit.length === 0) {
|
|
256
|
+
throw new Error(`${where} declares rateLimit as an empty array — declare at least one bucket or drop the option.`);
|
|
257
|
+
}
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
for (const { per } of rateLimit) {
|
|
260
|
+
if (seen.has(per)) {
|
|
261
|
+
throw new Error(`${where} declares rateLimit with per "${per}" more than once — both entries would share one ` +
|
|
262
|
+
`Redis bucket with conflicting limits. Keep one entry per "per".`);
|
|
263
|
+
}
|
|
264
|
+
seen.add(per);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
239
267
|
// No session ever carries the role "all" — `roles: ["all"]` is unreachable dead config, not a wildcard.
|
|
240
268
|
export function validateNoAllRoleInHandlerAccess(featureName, kind, handlerName, access) {
|
|
241
269
|
// skip: openToAll has no roles list to check
|
|
@@ -260,19 +288,22 @@ export function validateAnonymousRateLimit(featureName, kind, handlerName, acces
|
|
|
260
288
|
// skip: disabled declarations carry no `.per` to bucket on
|
|
261
289
|
if (isRateLimitDisabled(rateLimit))
|
|
262
290
|
return;
|
|
263
|
-
|
|
291
|
+
const options = declaredRateLimitOptions(rateLimit);
|
|
292
|
+
if (options.length === 0) {
|
|
264
293
|
throw new Error(`${kind} handler "${featureName}:${kind}:${handlerName}" allows anonymous callers but declares no ` +
|
|
265
294
|
`rateLimit — an anonymous, internet-facing endpoint needs an upper bound. Set rateLimit: ` +
|
|
266
295
|
`{ per: "ip", limit: ..., windowSeconds: ... }, or the documented exception ` +
|
|
267
296
|
`rateLimit: { disabled: true, reason: "..." }.`);
|
|
268
297
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
298
|
+
for (const { per } of options) {
|
|
299
|
+
// skip: this bucket is keyed on something safe (ip / tenant)
|
|
300
|
+
if (!USER_BUCKETED_RATE_LIMIT_PER.has(per))
|
|
301
|
+
continue;
|
|
302
|
+
throw new Error(`${kind} handler "${featureName}:${kind}:${handlerName}" allows anonymous callers but uses ` +
|
|
303
|
+
`rateLimit.per="${per}" — every anonymous request shares user.id="anonymous", ` +
|
|
304
|
+
`so this bucket would be a single global tap any caller could drain. ` +
|
|
305
|
+
`Use rateLimit.per="ip" or "ip+handler" for anonymous endpoints.`);
|
|
306
|
+
}
|
|
276
307
|
}
|
|
277
308
|
// --- MultiStreamProjection delivery-invariant ---
|
|
278
309
|
//
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { type EventStoreExecutor } from "../db/event-store-executor.ts";
|
|
3
|
-
import { type EntityTable } from "../db/table-builder.ts";
|
|
3
|
+
import { type EntityRow, type EntityTable } from "../db/table-builder.ts";
|
|
4
4
|
import type { EntityCrudRegistrar, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions } from "./types/entity-handlers.ts";
|
|
5
5
|
import type { AccessRule, EntityDefinition, EscapeHatchDeclaration, QueryHandlerDef, WriteHandlerDef } from "./types/index.ts";
|
|
6
6
|
export type { EntityCrudHandlerDefaults, EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "./types/entity-handlers.ts";
|
|
@@ -53,9 +53,9 @@ export declare function defineEntityDeleteHandler(entityName: string, entity: En
|
|
|
53
53
|
export declare function defineEntityRestoreHandler(entityName: string, entity: EntityDefinition, options: EntityHandlerOptions): WriteHandlerDef;
|
|
54
54
|
export declare function defineEntityListHandler(entityName: string, entity: EntityDefinition, options: EntityQueryHandlerOptions): QueryHandlerDef;
|
|
55
55
|
export declare function defineEntityDetailHandler(entityName: string, entity: EntityDefinition, options: EntityQueryHandlerOptions): QueryHandlerDef;
|
|
56
|
-
export declare function createEntityExecutor(entityName: string, entity:
|
|
56
|
+
export declare function createEntityExecutor<E extends EntityDefinition>(entityName: string, entity: E): {
|
|
57
57
|
readonly table: EntityTable;
|
|
58
|
-
readonly executor: EventStoreExecutor
|
|
58
|
+
readonly executor: EventStoreExecutor<EntityRow<E>>;
|
|
59
59
|
};
|
|
60
60
|
export declare function defineProjectionQueryHandler(name: string, projectionQualifiedName: string, options: {
|
|
61
61
|
access: AccessRule;
|
|
@@ -32,6 +32,7 @@ export declare function createBooleanField<R extends true | false = false>(overr
|
|
|
32
32
|
required?: R;
|
|
33
33
|
}): BooleanFieldDef & {
|
|
34
34
|
required: R;
|
|
35
|
+
default: boolean;
|
|
35
36
|
};
|
|
36
37
|
export declare function createSelectField<const TOptions extends readonly string[], R extends true | false = false>(opts: {
|
|
37
38
|
options: TOptions;
|
|
@@ -171,7 +172,7 @@ export declare function createFileField(overrides?: Partial<Omit<FileFieldDef, "
|
|
|
171
172
|
export declare function createImageField(overrides?: Partial<Omit<ImageFieldDef, "type" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind): ImageFieldDef;
|
|
172
173
|
export declare function createFilesField(overrides?: Partial<Omit<FilesFieldDef, "type" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind): FilesFieldDef;
|
|
173
174
|
export declare function createImagesField(overrides?: Partial<Omit<ImagesFieldDef, "type" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind): ImagesFieldDef;
|
|
174
|
-
export declare function createEntity<F, const T extends EntityTenancy = "tenant">(def: {
|
|
175
|
+
export declare function createEntity<const F, const T extends EntityTenancy = "tenant">(def: {
|
|
175
176
|
readonly table?: string;
|
|
176
177
|
readonly fields: F;
|
|
177
178
|
readonly description?: string;
|
package/dist/engine/factories.js
CHANGED
|
@@ -378,6 +378,8 @@ export function createImagesField(overrides) {
|
|
|
378
378
|
// Issue um Constraint-Inferenz auf strukturellen Records. Stattdessen
|
|
379
379
|
// validieren wir die Conformance via conditional return type: wenn F
|
|
380
380
|
// nicht FieldsMap-compat ist, kollabiert der Return zu `never`.
|
|
381
|
+
// `const F`: inline field literals (`{ type: "reference", ... }`) keep their
|
|
382
|
+
// `type` literal without `as const`; otherwise it widens to string → `never`.
|
|
381
383
|
export function createEntity(def) {
|
|
382
384
|
return {
|
|
383
385
|
softDelete: false,
|
|
@@ -178,7 +178,7 @@ function readHandlerHeaderFields(obj, methodName, sourceFile) {
|
|
|
178
178
|
const accessResult = readHeaderField(readObjectPropertyInitializer(obj, "access"), readOptionalAccessRule, methodName, sourceFile, "access must be a recognized AccessRule ({ roles: [...] } or { openToAll: { reason } }), or a reference to one");
|
|
179
179
|
if (accessResult.kind === "error")
|
|
180
180
|
return accessResult;
|
|
181
|
-
const rateLimitResult = readHeaderField(readObjectPropertyInitializer(obj, "rateLimit"), readOptionalRateLimit, methodName, sourceFile, "rateLimit must be { per, limit, windowSeconds } or { disabled: true, reason }, or a reference to one");
|
|
181
|
+
const rateLimitResult = readHeaderField(readObjectPropertyInitializer(obj, "rateLimit"), readOptionalRateLimit, methodName, sourceFile, "rateLimit must be { per, limit, windowSeconds }, a non-empty list of those, or { disabled: true, reason }, or a reference to one");
|
|
182
182
|
if (rateLimitResult.kind === "error")
|
|
183
183
|
return rateLimitResult;
|
|
184
184
|
const additionalRateLimitsResult = readHeaderField(readObjectPropertyInitializer(obj, "additionalRateLimits"), readOptionalAdditionalRateLimits, methodName, sourceFile, "additionalRateLimits must be an array of { per: { payloadField }, limit, windowSeconds }, or a reference to one");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEscapeHatchGrant } from "@cosmicdrift/kumiko-types/handlers";
|
|
1
|
+
import { isEscapeHatchGrant, isRateLimitOption } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
2
|
import { SyntaxKind } from "ts-morph";
|
|
3
3
|
import { sourceLocationFromNode } from "../source-location.js";
|
|
4
4
|
import { containsRawRefSentinel, fail, findFunctionLiteral, isPlainObject, ok, readDataLiteralNode, readNameLiteral, readNameOrRef, readNameOrRefOrList, readObjectPropertyInitializer, } from "./shared.js";
|
|
@@ -52,6 +52,12 @@ export function readOptionalAccessRule(value) {
|
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
54
|
export function readOptionalRateLimit(value) {
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
if (!value.every(isRateLimitOption))
|
|
57
|
+
return undefined;
|
|
58
|
+
const [first, ...rest] = value;
|
|
59
|
+
return first === undefined ? undefined : [first, ...rest];
|
|
60
|
+
}
|
|
55
61
|
if (!isPlainObject(value))
|
|
56
62
|
return undefined;
|
|
57
63
|
if (value["disabled"] === true) {
|
|
@@ -63,13 +69,7 @@ export function readOptionalRateLimit(value) {
|
|
|
63
69
|
return undefined;
|
|
64
70
|
return { disabled: true, reason: value["reason"] };
|
|
65
71
|
}
|
|
66
|
-
|
|
67
|
-
return undefined;
|
|
68
|
-
if (typeof value["limit"] !== "number")
|
|
69
|
-
return undefined;
|
|
70
|
-
if (typeof value["windowSeconds"] !== "number")
|
|
71
|
-
return undefined;
|
|
72
|
-
return value;
|
|
72
|
+
return isRateLimitOption(value) ? value : undefined;
|
|
73
73
|
}
|
|
74
74
|
function isPayloadRateLimitOption(value) {
|
|
75
75
|
if (!isPlainObject(value))
|
|
@@ -189,7 +189,31 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
|
|
|
189
189
|
limit: z.ZodNumber;
|
|
190
190
|
windowSeconds: z.ZodNumber;
|
|
191
191
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
192
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
192
|
+
}, z.core.$strict>, z.ZodTuple<[z.ZodObject<{
|
|
193
|
+
per: z.ZodEnum<{
|
|
194
|
+
ip: "ip";
|
|
195
|
+
"ip+handler": "ip+handler";
|
|
196
|
+
tenant: "tenant";
|
|
197
|
+
"tenant+handler": "tenant+handler";
|
|
198
|
+
user: "user";
|
|
199
|
+
"user+handler": "user+handler";
|
|
200
|
+
}>;
|
|
201
|
+
limit: z.ZodNumber;
|
|
202
|
+
windowSeconds: z.ZodNumber;
|
|
203
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
204
|
+
}, z.core.$strict>], z.ZodObject<{
|
|
205
|
+
per: z.ZodEnum<{
|
|
206
|
+
ip: "ip";
|
|
207
|
+
"ip+handler": "ip+handler";
|
|
208
|
+
tenant: "tenant";
|
|
209
|
+
"tenant+handler": "tenant+handler";
|
|
210
|
+
user: "user";
|
|
211
|
+
"user+handler": "user+handler";
|
|
212
|
+
}>;
|
|
213
|
+
limit: z.ZodNumber;
|
|
214
|
+
windowSeconds: z.ZodNumber;
|
|
215
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
216
|
+
}, z.core.$strict>>, z.ZodObject<{
|
|
193
217
|
disabled: z.ZodLiteral<true>;
|
|
194
218
|
reason: z.ZodString;
|
|
195
219
|
}, z.core.$strict>]>, z.ZodObject<{
|
|
@@ -257,7 +281,31 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
|
|
|
257
281
|
limit: z.ZodNumber;
|
|
258
282
|
windowSeconds: z.ZodNumber;
|
|
259
283
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
260
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
284
|
+
}, z.core.$strict>, z.ZodTuple<[z.ZodObject<{
|
|
285
|
+
per: z.ZodEnum<{
|
|
286
|
+
ip: "ip";
|
|
287
|
+
"ip+handler": "ip+handler";
|
|
288
|
+
tenant: "tenant";
|
|
289
|
+
"tenant+handler": "tenant+handler";
|
|
290
|
+
user: "user";
|
|
291
|
+
"user+handler": "user+handler";
|
|
292
|
+
}>;
|
|
293
|
+
limit: z.ZodNumber;
|
|
294
|
+
windowSeconds: z.ZodNumber;
|
|
295
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
296
|
+
}, z.core.$strict>], z.ZodObject<{
|
|
297
|
+
per: z.ZodEnum<{
|
|
298
|
+
ip: "ip";
|
|
299
|
+
"ip+handler": "ip+handler";
|
|
300
|
+
tenant: "tenant";
|
|
301
|
+
"tenant+handler": "tenant+handler";
|
|
302
|
+
user: "user";
|
|
303
|
+
"user+handler": "user+handler";
|
|
304
|
+
}>;
|
|
305
|
+
limit: z.ZodNumber;
|
|
306
|
+
windowSeconds: z.ZodNumber;
|
|
307
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
308
|
+
}, z.core.$strict>>, z.ZodObject<{
|
|
261
309
|
disabled: z.ZodLiteral<true>;
|
|
262
310
|
reason: z.ZodString;
|
|
263
311
|
}, z.core.$strict>]>, z.ZodObject<{
|
|
@@ -303,7 +351,31 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
|
|
|
303
351
|
limit: z.ZodNumber;
|
|
304
352
|
windowSeconds: z.ZodNumber;
|
|
305
353
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
306
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
354
|
+
}, z.core.$strict>, z.ZodTuple<[z.ZodObject<{
|
|
355
|
+
per: z.ZodEnum<{
|
|
356
|
+
ip: "ip";
|
|
357
|
+
"ip+handler": "ip+handler";
|
|
358
|
+
tenant: "tenant";
|
|
359
|
+
"tenant+handler": "tenant+handler";
|
|
360
|
+
user: "user";
|
|
361
|
+
"user+handler": "user+handler";
|
|
362
|
+
}>;
|
|
363
|
+
limit: z.ZodNumber;
|
|
364
|
+
windowSeconds: z.ZodNumber;
|
|
365
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
366
|
+
}, z.core.$strict>], z.ZodObject<{
|
|
367
|
+
per: z.ZodEnum<{
|
|
368
|
+
ip: "ip";
|
|
369
|
+
"ip+handler": "ip+handler";
|
|
370
|
+
tenant: "tenant";
|
|
371
|
+
"tenant+handler": "tenant+handler";
|
|
372
|
+
user: "user";
|
|
373
|
+
"user+handler": "user+handler";
|
|
374
|
+
}>;
|
|
375
|
+
limit: z.ZodNumber;
|
|
376
|
+
windowSeconds: z.ZodNumber;
|
|
377
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
378
|
+
}, z.core.$strict>>, z.ZodObject<{
|
|
307
379
|
disabled: z.ZodLiteral<true>;
|
|
308
380
|
reason: z.ZodString;
|
|
309
381
|
}, z.core.$strict>]>, z.ZodObject<{
|
|
@@ -712,7 +784,31 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
712
784
|
limit: z.ZodNumber;
|
|
713
785
|
windowSeconds: z.ZodNumber;
|
|
714
786
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
715
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
787
|
+
}, z.core.$strict>, z.ZodTuple<[z.ZodObject<{
|
|
788
|
+
per: z.ZodEnum<{
|
|
789
|
+
ip: "ip";
|
|
790
|
+
"ip+handler": "ip+handler";
|
|
791
|
+
tenant: "tenant";
|
|
792
|
+
"tenant+handler": "tenant+handler";
|
|
793
|
+
user: "user";
|
|
794
|
+
"user+handler": "user+handler";
|
|
795
|
+
}>;
|
|
796
|
+
limit: z.ZodNumber;
|
|
797
|
+
windowSeconds: z.ZodNumber;
|
|
798
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
799
|
+
}, z.core.$strict>], z.ZodObject<{
|
|
800
|
+
per: z.ZodEnum<{
|
|
801
|
+
ip: "ip";
|
|
802
|
+
"ip+handler": "ip+handler";
|
|
803
|
+
tenant: "tenant";
|
|
804
|
+
"tenant+handler": "tenant+handler";
|
|
805
|
+
user: "user";
|
|
806
|
+
"user+handler": "user+handler";
|
|
807
|
+
}>;
|
|
808
|
+
limit: z.ZodNumber;
|
|
809
|
+
windowSeconds: z.ZodNumber;
|
|
810
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
811
|
+
}, z.core.$strict>>, z.ZodObject<{
|
|
716
812
|
disabled: z.ZodLiteral<true>;
|
|
717
813
|
reason: z.ZodString;
|
|
718
814
|
}, z.core.$strict>]>, z.ZodObject<{
|
|
@@ -779,7 +875,31 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
779
875
|
limit: z.ZodNumber;
|
|
780
876
|
windowSeconds: z.ZodNumber;
|
|
781
877
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
782
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
878
|
+
}, z.core.$strict>, z.ZodTuple<[z.ZodObject<{
|
|
879
|
+
per: z.ZodEnum<{
|
|
880
|
+
ip: "ip";
|
|
881
|
+
"ip+handler": "ip+handler";
|
|
882
|
+
tenant: "tenant";
|
|
883
|
+
"tenant+handler": "tenant+handler";
|
|
884
|
+
user: "user";
|
|
885
|
+
"user+handler": "user+handler";
|
|
886
|
+
}>;
|
|
887
|
+
limit: z.ZodNumber;
|
|
888
|
+
windowSeconds: z.ZodNumber;
|
|
889
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
890
|
+
}, z.core.$strict>], z.ZodObject<{
|
|
891
|
+
per: z.ZodEnum<{
|
|
892
|
+
ip: "ip";
|
|
893
|
+
"ip+handler": "ip+handler";
|
|
894
|
+
tenant: "tenant";
|
|
895
|
+
"tenant+handler": "tenant+handler";
|
|
896
|
+
user: "user";
|
|
897
|
+
"user+handler": "user+handler";
|
|
898
|
+
}>;
|
|
899
|
+
limit: z.ZodNumber;
|
|
900
|
+
windowSeconds: z.ZodNumber;
|
|
901
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
902
|
+
}, z.core.$strict>>, z.ZodObject<{
|
|
783
903
|
disabled: z.ZodLiteral<true>;
|
|
784
904
|
reason: z.ZodString;
|
|
785
905
|
}, z.core.$strict>]>, z.ZodObject<{
|
|
@@ -824,7 +944,31 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
824
944
|
limit: z.ZodNumber;
|
|
825
945
|
windowSeconds: z.ZodNumber;
|
|
826
946
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
827
|
-
}, z.core.$strict>, z.ZodObject<{
|
|
947
|
+
}, z.core.$strict>, z.ZodTuple<[z.ZodObject<{
|
|
948
|
+
per: z.ZodEnum<{
|
|
949
|
+
ip: "ip";
|
|
950
|
+
"ip+handler": "ip+handler";
|
|
951
|
+
tenant: "tenant";
|
|
952
|
+
"tenant+handler": "tenant+handler";
|
|
953
|
+
user: "user";
|
|
954
|
+
"user+handler": "user+handler";
|
|
955
|
+
}>;
|
|
956
|
+
limit: z.ZodNumber;
|
|
957
|
+
windowSeconds: z.ZodNumber;
|
|
958
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
959
|
+
}, z.core.$strict>], z.ZodObject<{
|
|
960
|
+
per: z.ZodEnum<{
|
|
961
|
+
ip: "ip";
|
|
962
|
+
"ip+handler": "ip+handler";
|
|
963
|
+
tenant: "tenant";
|
|
964
|
+
"tenant+handler": "tenant+handler";
|
|
965
|
+
user: "user";
|
|
966
|
+
"user+handler": "user+handler";
|
|
967
|
+
}>;
|
|
968
|
+
limit: z.ZodNumber;
|
|
969
|
+
windowSeconds: z.ZodNumber;
|
|
970
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
971
|
+
}, z.core.$strict>>, z.ZodObject<{
|
|
828
972
|
disabled: z.ZodLiteral<true>;
|
|
829
973
|
reason: z.ZodString;
|
|
830
974
|
}, z.core.$strict>]>, z.ZodObject<{
|
|
@@ -157,6 +157,7 @@ const rateLimitOptionSchema = z
|
|
|
157
157
|
cost: z.number().optional(),
|
|
158
158
|
})
|
|
159
159
|
.strict();
|
|
160
|
+
const rateLimitOptionListSchema = z.tuple([rateLimitOptionSchema], rateLimitOptionSchema);
|
|
160
161
|
const additionalRateLimitsSchema = z.array(z
|
|
161
162
|
.object({
|
|
162
163
|
per: z.object({ payloadField: z.string() }).strict(),
|
|
@@ -247,7 +248,7 @@ const WRITE_HANDLER_PATTERN_HEADER_SHAPE = {
|
|
|
247
248
|
...WRITE_HANDLER_HEADER_SHAPE,
|
|
248
249
|
access: orRawRef(accessRuleSchema),
|
|
249
250
|
agent: orRawRef(agentHandlerHintsSchema),
|
|
250
|
-
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitDisabledSchema])),
|
|
251
|
+
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitOptionListSchema, rateLimitDisabledSchema])),
|
|
251
252
|
additionalRateLimits: orRawRef(additionalRateLimitsSchema),
|
|
252
253
|
escapeHatch: orRawRef(escapeHatchSchema),
|
|
253
254
|
};
|
|
@@ -255,13 +256,13 @@ const QUERY_HANDLER_PATTERN_HEADER_SHAPE = {
|
|
|
255
256
|
...QUERY_HANDLER_HEADER_SHAPE,
|
|
256
257
|
access: orRawRef(accessRuleSchema),
|
|
257
258
|
agent: orRawRef(agentHandlerHintsSchema),
|
|
258
|
-
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitDisabledSchema])),
|
|
259
|
+
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitOptionListSchema, rateLimitDisabledSchema])),
|
|
259
260
|
escapeHatch: orRawRef(escapeHatchSchema),
|
|
260
261
|
};
|
|
261
262
|
const STREAM_HANDLER_PATTERN_HEADER_SHAPE = {
|
|
262
263
|
...STREAM_HANDLER_HEADER_SHAPE,
|
|
263
264
|
access: orRawRef(accessRuleSchema),
|
|
264
|
-
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitDisabledSchema])),
|
|
265
|
+
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitOptionListSchema, rateLimitDisabledSchema])),
|
|
265
266
|
escapeHatch: orRawRef(escapeHatchSchema),
|
|
266
267
|
};
|
|
267
268
|
function requireHandlerBody(val, ctx, headerKeys) {
|
|
@@ -2,4 +2,5 @@ export declare const FORBIDDEN_MEMBERSHIP_ROLES: ReadonlySet<string>;
|
|
|
2
2
|
export declare function isForbiddenMembershipRole(role: string): boolean;
|
|
3
3
|
export declare function findForbiddenMembershipRole(roles: readonly string[]): string | undefined;
|
|
4
4
|
export declare function stripForbiddenMembershipRoles(roles: readonly string[]): readonly string[];
|
|
5
|
+
export declare function stripReservedTokenRoles(roles: readonly string[]): readonly string[];
|
|
5
6
|
export declare function buildSessionRoles(globalRoles: readonly string[], membershipRoles: readonly string[]): readonly string[];
|
|
@@ -38,6 +38,14 @@ const NON_MINTABLE_GLOBAL_ROLES = new Set([
|
|
|
38
38
|
function stripNonMintableGlobalRoles(roles) {
|
|
39
39
|
return roles.filter((role) => !NON_MINTABLE_GLOBAL_ROLES.has(role));
|
|
40
40
|
}
|
|
41
|
+
// Applied on JWT verify: a token signed with the right key (leaked or foreign)
|
|
42
|
+
// must not carry the in-process System identity or the "all" pseudo-role.
|
|
43
|
+
// "anonymous" stays: it only narrows access (openToAll excludes it), and
|
|
44
|
+
// stripping it would turn the token into an authenticated user with no roles.
|
|
45
|
+
const RESERVED_TOKEN_ROLES = new Set([...access.all, SYSTEM_ROLE]);
|
|
46
|
+
export function stripReservedTokenRoles(roles) {
|
|
47
|
+
return roles.filter((role) => !RESERVED_TOKEN_ROLES.has(role));
|
|
48
|
+
}
|
|
41
49
|
// Single mint path for session roles: merges globalRoles with the stripped
|
|
42
50
|
// membership portion and dedupes. Every place that builds a session's roles
|
|
43
51
|
// from a membership should go through this instead of repeating
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// Tenant-level counterpart to projectAppSchemaForRoles: removes the UI surface
|
|
2
2
|
// (screens, navs, content collections, workspaces) of features the tenant has
|
|
3
3
|
// switched off via r.toggleable(). Entities and translations stay — enabled
|
|
4
|
-
// features may still reference them.
|
|
4
|
+
// features may still reference them. Screens that declare a `fallback` (plus
|
|
5
|
+
// the same-feature screens those fallbacks name) are the exception: their
|
|
6
|
+
// visibleWhen probe is rejected by the feature gate for this tenant, which is
|
|
7
|
+
// what makes the renderer show the fallback (e.g. an upgrade notice). Dropping
|
|
8
|
+
// them would also drop every nav of another feature pointing at them, so the
|
|
9
|
+
// fallback could never be reached. Run BEFORE the role projection: it keeps
|
|
5
10
|
// only targets that exist in the schema, so once the disabled screens/navs are
|
|
6
11
|
// gone it also drops every action, redirect, nav and workspace member that
|
|
7
12
|
// pointed at them. The one reference it does not resolve is a nav `parent`
|
|
8
13
|
// pointing at a missing nav; that is pruned here.
|
|
14
|
+
import { resolveScreenTargetQn } from "./boot-validator/screens.js";
|
|
9
15
|
import { isToggleableFeature } from "./effective-features.js";
|
|
10
16
|
import { isValidQn, qualifyEntityName } from "./qualified-name.js";
|
|
11
17
|
// Sorted, so the result doubles as a stable cache key. The trial gate is
|
|
@@ -50,6 +56,28 @@ function collectOrphanedNavQns(features, removedNavQns) {
|
|
|
50
56
|
}
|
|
51
57
|
return orphaned;
|
|
52
58
|
}
|
|
59
|
+
// Fail-closed for the kept screens: the server gate rejects their handlers and
|
|
60
|
+
// queries for the tenant, only the schema entry stays so the fallback can render.
|
|
61
|
+
function screensKeptForFallback(feature) {
|
|
62
|
+
const qnOf = (screenId) => qualifyEntityName(feature.featureName, "screen", screenId);
|
|
63
|
+
const byQn = new Map(feature.screens.map((screen) => [qnOf(screen.id), screen]));
|
|
64
|
+
const kept = new Set();
|
|
65
|
+
const pending = feature.screens.filter((screen) => screen.fallback !== undefined);
|
|
66
|
+
for (const screen of pending)
|
|
67
|
+
kept.add(qnOf(screen.id));
|
|
68
|
+
while (pending.length > 0) {
|
|
69
|
+
const fallback = pending.pop()?.fallback;
|
|
70
|
+
if (fallback === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
const targetQn = resolveScreenTargetQn(feature.featureName, fallback);
|
|
73
|
+
const target = byQn.get(targetQn);
|
|
74
|
+
if (target === undefined || kept.has(targetQn))
|
|
75
|
+
continue;
|
|
76
|
+
kept.add(targetQn);
|
|
77
|
+
pending.push(target);
|
|
78
|
+
}
|
|
79
|
+
return feature.screens.filter((screen) => kept.has(qnOf(screen.id)));
|
|
80
|
+
}
|
|
53
81
|
export function stripDisabledFeaturesFromAppSchema(schema, registry, disabledFeatureNames) {
|
|
54
82
|
if (disabledFeatureNames.length === 0)
|
|
55
83
|
return schema;
|
|
@@ -66,7 +94,7 @@ export function stripDisabledFeaturesFromAppSchema(schema, registry, disabledFea
|
|
|
66
94
|
const features = schema.features.map((feature) => {
|
|
67
95
|
if (disabled.has(feature.featureName)) {
|
|
68
96
|
const { navs: _navs, contentCollections: _collections, workspaces: _ws, ...rest } = feature;
|
|
69
|
-
return { ...rest, screens:
|
|
97
|
+
return { ...rest, screens: screensKeptForFallback(feature) };
|
|
70
98
|
}
|
|
71
99
|
if (orphanedNavQns.size === 0 || feature.navs === undefined)
|
|
72
100
|
return feature;
|