@cosmicdrift/kumiko-framework 0.333.0 → 0.335.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/api/auth-routes.js +19 -4
- package/dist/api/client-ip.js +3 -6
- package/dist/api/extra-route.js +3 -2
- package/dist/api/pii-leak-guard.js +7 -4
- package/dist/api/redis-sse-broker.d.ts +2 -0
- package/dist/api/redis-sse-broker.js +6 -7
- package/dist/api/routes.js +16 -17
- package/dist/api/server.js +14 -7
- package/dist/api/sse-broker.d.ts +3 -0
- package/dist/api/sse-broker.js +14 -0
- package/dist/api/sse-route.d.ts +1 -1
- package/dist/api/sse-route.js +39 -6
- package/dist/bun-db/connection.js +0 -2
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +1 -0
- package/dist/bun-db/query.js +15 -4
- package/dist/crypto/kek-source.js +40 -36
- package/dist/crypto/kms-adapter.js +2 -2
- package/dist/db/apply-entity-event.js +8 -2
- package/dist/db/blind-index-cleanup.js +11 -1
- package/dist/db/eagerload.js +2 -1
- package/dist/db/entity-table-meta.js +5 -3
- package/dist/db/event-store-executor-context.js +13 -19
- package/dist/db/event-store-executor-read.js +39 -36
- package/dist/db/event-store-executor-write.js +15 -17
- package/dist/db/migrate-generator.js +27 -28
- package/dist/db/pg-error.js +9 -5
- package/dist/db/queries/backfill-pii.js +15 -5
- package/dist/db/queries/shadow-swap.js +5 -4
- package/dist/db/table-builder.d.ts +1 -0
- package/dist/db/table-builder.js +7 -15
- package/dist/db/tenant-db.js +18 -2
- package/dist/derivatives/variant-key.js +3 -1
- package/dist/engine/boot-validator/access-declarations.js +12 -3
- package/dist/engine/boot-validator/config-deps.js +13 -0
- package/dist/engine/boot-validator/entity-handler.js +13 -1
- package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/index.js +6 -6
- package/dist/engine/boot-validator/nav.d.ts +2 -1
- package/dist/engine/boot-validator/nav.js +10 -20
- package/dist/engine/boot-validator/ownership.d.ts +1 -1
- package/dist/engine/boot-validator/ownership.js +2 -1
- package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
- package/dist/engine/boot-validator/pii-retention.js +44 -4
- package/dist/engine/boot-validator/projection-list-screens.js +8 -0
- package/dist/engine/boot-validator/query-output-columns.js +41 -5
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/screens.js +138 -36
- package/dist/engine/boot-validator/transfer-graph.js +9 -6
- package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
- package/dist/engine/boot-validator/zod-shape.js +17 -4
- package/dist/engine/build-app-schema.js +65 -19
- package/dist/engine/build-config-feature-schema.js +106 -22
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/config-helpers.d.ts +2 -2
- package/dist/engine/entity-handlers.js +7 -2
- package/dist/engine/feature-ast/extractors/events.js +11 -8
- package/dist/engine/feature-ast/extractors/handlers.js +38 -16
- package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
- package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
- package/dist/engine/feature-ast/parse.d.ts +1 -0
- package/dist/engine/feature-ast/parse.js +18 -3
- package/dist/engine/feature-ast/patch.js +80 -61
- package/dist/engine/feature-ast/patcher.d.ts +1 -0
- package/dist/engine/feature-ast/patcher.js +2 -1
- package/dist/engine/feature-ast/render.js +1 -1
- package/dist/engine/feature-entity-handlers.js +22 -0
- package/dist/engine/field-access.d.ts +2 -0
- package/dist/engine/field-access.js +40 -0
- package/dist/engine/index.d.ts +5 -4
- package/dist/engine/index.js +4 -3
- package/dist/engine/ownership.d.ts +1 -0
- package/dist/engine/ownership.js +15 -2
- package/dist/engine/pattern-library/mixed-schemas.js +15 -29
- package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
- package/dist/engine/pattern-library/shared-fields.js +17 -0
- package/dist/engine/registry-facade.js +3 -0
- package/dist/engine/role-assignment.d.ts +7 -1
- package/dist/engine/role-assignment.js +27 -7
- package/dist/engine/schema-builder.d.ts +2 -0
- package/dist/engine/schema-builder.js +20 -3
- package/dist/engine/screen-helpers.js +15 -4
- package/dist/engine/steps/_event-match.js +22 -3
- package/dist/engine/steps/_read-source.js +8 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/engine/where-rule-lint.js +22 -4
- package/dist/env/index.js +21 -1
- package/dist/errors/reasons.d.ts +0 -1
- package/dist/errors/reasons.js +2 -5
- package/dist/event-store/event-store.js +6 -1
- package/dist/event-store/provenance-append.js +4 -1
- package/dist/files/file-ref-entity.d.ts +6 -0
- package/dist/files/file-ref-table.d.ts +6 -0
- package/dist/files/file-routes.d.ts +0 -1
- package/dist/files/file-routes.js +22 -7
- package/dist/files/local-provider.js +13 -10
- package/dist/files/provider-resolver.js +20 -1
- package/dist/files/types.js +16 -1
- package/dist/i18n/request-locale.d.ts +3 -2
- package/dist/i18n/request-locale.js +16 -6
- package/dist/i18n/required-surface-keys.js +28 -35
- package/dist/i18n/settings-hub-keys.js +21 -4
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/job-runner.d.ts +2 -0
- package/dist/jobs/job-runner.js +150 -59
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/pending-rebuilds.d.ts +4 -0
- package/dist/migrations/pending-rebuilds.js +23 -1
- package/dist/observability/escape-hatch-report.d.ts +1 -0
- package/dist/observability/escape-hatch-report.js +3 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/pipeline/dispatch-batch.js +8 -3
- package/dist/pipeline/dispatch-query.d.ts +1 -1
- package/dist/pipeline/dispatch-query.js +2 -2
- package/dist/pipeline/dispatch-shared.d.ts +2 -3
- package/dist/pipeline/dispatch-shared.js +14 -9
- package/dist/pipeline/dispatch-stream.js +2 -5
- package/dist/pipeline/dispatch-write.js +27 -7
- package/dist/pipeline/dispatcher-utils.js +4 -1
- package/dist/pipeline/dispatcher.js +1 -2
- package/dist/pipeline/event-dispatcher-admin.js +12 -2
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +26 -11
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/member-read-only-transaction.js +1 -14
- package/dist/pipeline/msp-rebuild.d.ts +1 -0
- package/dist/pipeline/msp-rebuild.js +1 -0
- package/dist/pipeline/system-hooks.d.ts +1 -1
- package/dist/pipeline/system-hooks.js +9 -7
- package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
- package/dist/pipeline/tenant-timezone-cache.js +20 -6
- package/dist/pipeline/write-result-masking.d.ts +6 -0
- package/dist/pipeline/write-result-masking.js +36 -0
- package/dist/redis/pubsub-signal.d.ts +2 -0
- package/dist/redis/pubsub-signal.js +2 -2
- package/dist/schema-cli.js +4 -14
- package/dist/search/meilisearch-adapter.js +17 -6
- package/dist/search/purge-subject.js +8 -3
- package/dist/stack/drain-jobs.js +24 -12
- package/dist/stack/table-helpers.js +10 -8
- package/dist/testing/closed-connection-error.js +2 -3
- package/dist/testing/shared-entities.d.ts +10 -0
- package/dist/testing/wait-for.js +1 -0
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.d.ts +1 -0
- package/dist/upgrade-cli.js +36 -15
- package/package.json +4 -4
- package/src/changes.json +218 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
- package/src/scripts/codemod/migrate-db-raw.ts +16 -8
- package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
- package/src/scripts/codemod/pii-personal-migration.ts +56 -19
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +83 -26
- package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
- package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
- package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
- package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
- package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
- package/dist/scripts/codemod/migrate-db-raw.js +0 -345
- package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
- package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
- package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
- package/dist/scripts/codemod/pii-personal-migration.js +0 -615
- package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
- package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
|
@@ -81,19 +81,28 @@ function validatePersonalDataValue(feature, handlerName, access) {
|
|
|
81
81
|
`access.personalData ${JSON.stringify(declared)} — the only supported value is "public-intake".`);
|
|
82
82
|
}
|
|
83
83
|
// skip: roles include "anonymous", which personalData: "public-intake" requires
|
|
84
|
-
if (
|
|
84
|
+
if (accessAllowsAnonymous(access))
|
|
85
85
|
return;
|
|
86
86
|
throw new Error(`[Feature ${feature.name}] write handler "${handlerName}" declares ` +
|
|
87
87
|
'access.personalData: "public-intake" but its roles do not include ' +
|
|
88
88
|
`"${ANONYMOUS_ROLE}" — personalData: "public-intake" only applies to handlers ` +
|
|
89
89
|
"that allow anonymous callers.");
|
|
90
90
|
}
|
|
91
|
+
function handlerInputKeys(feature, handlerName, handler) {
|
|
92
|
+
try {
|
|
93
|
+
return collectZodObjectKeys(handler.schema);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
97
|
+
throw new Error(`[Feature ${feature.name}] write handler "${handlerName}": ${reason}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
91
100
|
function validateOpenToAllPersonalData(feature, handlerName, handler) {
|
|
92
101
|
const access = handler.access;
|
|
93
102
|
// skip: no openToAll declared, or the handler declares tenant members may write personal data
|
|
94
103
|
if (!hasOpenToAll(access) || declaresTenantMembersPersonalData(access))
|
|
95
104
|
return;
|
|
96
|
-
const inputKeys =
|
|
105
|
+
const inputKeys = handlerInputKeys(feature, handlerName, handler);
|
|
97
106
|
const personalNames = candidatePersonalFieldNames(feature, handlerName, handler);
|
|
98
107
|
const offending = [...inputKeys].filter((key) => personalNames.has(key));
|
|
99
108
|
// skip: no personal-data fields in the handler's input
|
|
@@ -114,7 +123,7 @@ function validateAnonymousPersonalData(feature, handlerName, handler) {
|
|
|
114
123
|
// skip: not reachable by an anonymous caller, or already declares public-intake
|
|
115
124
|
if (!accessAllowsAnonymous(access) || declaredPersonalData(access) === "public-intake")
|
|
116
125
|
return;
|
|
117
|
-
const inputKeys =
|
|
126
|
+
const inputKeys = handlerInputKeys(feature, handlerName, handler);
|
|
118
127
|
const personalNames = candidatePersonalFieldNames(feature, handlerName, handler, false);
|
|
119
128
|
const offending = [...inputKeys].filter((key) => personalNames.has(key));
|
|
120
129
|
// skip: no personal-data fields in the handler's input
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { optionsQueryFieldRefs } from "@cosmicdrift/kumiko-types/fields";
|
|
1
2
|
import { isEncryptedAtRest } from "../config-helpers.js";
|
|
2
3
|
// --- Toggleable-dependency warnings ---
|
|
3
4
|
//
|
|
@@ -123,6 +124,18 @@ export function validateConfigKeyOptionsQuery(feature) {
|
|
|
123
124
|
if (keyDef.allowPerRequest) {
|
|
124
125
|
throw new Error(`${prefix} has optionsQuery AND allowPerRequest=true — a per-request value would bypass the option set unchecked`);
|
|
125
126
|
}
|
|
127
|
+
validateOptionsQueryKeyRefs(feature, keyName, keyDef);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function validateOptionsQueryKeyRefs(feature, keyName, keyDef) {
|
|
131
|
+
const prefix = `[Feature ${feature.name}] Config key "${keyName}"`;
|
|
132
|
+
for (const ref of optionsQueryFieldRefs(keyDef.optionsQueryPayload)) {
|
|
133
|
+
if (ref === keyName) {
|
|
134
|
+
throw new Error(`${prefix} optionsQueryPayload references itself ({ field: "${ref}" })`);
|
|
135
|
+
}
|
|
136
|
+
if (feature.configKeys[ref] === undefined) {
|
|
137
|
+
throw new Error(`${prefix} optionsQueryPayload references unknown config key "${ref}" of feature "${feature.name}"`);
|
|
138
|
+
}
|
|
126
139
|
}
|
|
127
140
|
}
|
|
128
141
|
// --- Config key storage backing × scope matrix ---
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRateLimitDisabled } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
2
|
import { VARIANT_NAME_PATTERN } from "../../derivatives/variant-key.js";
|
|
3
|
+
import { access as accessPresets } from "../config-helpers.js";
|
|
3
4
|
import { parseRefTarget } from "../parse-ref-target.js";
|
|
4
5
|
export const FILE_FIELD_TYPES = new Set(["file", "image", "files", "images"]);
|
|
5
6
|
// Field-Namen die typischerweise PII enthalten. Ohne `pii: true` /
|
|
@@ -150,7 +151,7 @@ export function validateNoAllRoleInHandlerAccess(featureName, kind, handlerName,
|
|
|
150
151
|
if (!("roles" in access))
|
|
151
152
|
return;
|
|
152
153
|
// skip: no "all" in the roles list, nothing unreachable to report
|
|
153
|
-
if (!access.roles.includes(
|
|
154
|
+
if (!accessPresets.all.some((role) => access.roles.includes(role)))
|
|
154
155
|
return;
|
|
155
156
|
throw new Error(`${kind} handler "${featureName}:${kind}:${handlerName}" declares access: { roles: ["all"] } — ` +
|
|
156
157
|
`no session ever carries the role "all", so this handler is unreachable by any caller. ` +
|
|
@@ -578,8 +579,19 @@ function validateEmbeddedListMetadata(fieldName, entityName, field, entity) {
|
|
|
578
579
|
// default — wenn gesetzt — ist eine Teilmenge der options. Beides würde
|
|
579
580
|
// auch im Zod-Schema bei runtime fehlschlagen, der Boot-Catch ist nur
|
|
580
581
|
// die früheste Stelle für klare Fehlermeldungen.
|
|
582
|
+
// Exhaustive over MultiSelectFieldDef["columns"] (the compiler flags a union
|
|
583
|
+
// change); the TS union is gone at runtime for JS/JSON-sourced definitions.
|
|
584
|
+
const CHECKBOX_COLUMN_COUNTS = {
|
|
585
|
+
1: true,
|
|
586
|
+
2: true,
|
|
587
|
+
3: true,
|
|
588
|
+
4: true,
|
|
589
|
+
};
|
|
581
590
|
function validateCheckboxDisplayOptions(field, fieldName, entityName) {
|
|
582
591
|
if (field.display === "checkboxes") {
|
|
592
|
+
if (field.columns !== undefined && !(field.columns in CHECKBOX_COLUMN_COUNTS)) {
|
|
593
|
+
throw new Error(`MultiSelect field "${fieldName}" on entity "${entityName}" has invalid columns "${field.columns}" — must be ${Object.keys(CHECKBOX_COLUMN_COUNTS).join(", ")}.`);
|
|
594
|
+
}
|
|
583
595
|
if (field.maxRows !== undefined && (!Number.isInteger(field.maxRows) || field.maxRows < 1)) {
|
|
584
596
|
throw new Error(`MultiSelect field "${fieldName}" on entity "${entityName}" has invalid maxRows "${field.maxRows}" — must be a positive integer.`);
|
|
585
597
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FeatureDefinition } from "../types/index.js";
|
|
2
2
|
/** Operator lists default searchable; low-cardinality audit trails stay opt-out. */
|
|
3
3
|
export declare const SEARCHABLE_FALSE_WHITELIST: Set<string>;
|
|
4
|
+
export declare const LIST_ROW_META_COLUMN_NAMES: Set<string>;
|
|
4
5
|
export declare function validateEntityListScreens(features: readonly FeatureDefinition[]): void;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { LIST_ROW_META_COLUMNS } from "../../ui-types/list-row-meta.js";
|
|
2
2
|
import { normalizeListColumn } from "../screen-helpers.js";
|
|
3
3
|
/** Operator lists default searchable; low-cardinality audit trails stay opt-out. */
|
|
4
|
-
export const SEARCHABLE_FALSE_WHITELIST = new Set(["download-attempt-list"]);
|
|
4
|
+
export const SEARCHABLE_FALSE_WHITELIST = new Set(["download-attempt-list", "cap-list"]);
|
|
5
5
|
// Same set computeListViewModel (headless) can actually render — NOT
|
|
6
6
|
// db/table-builder's rowMetaFieldNames(softDelete), which also carries
|
|
7
7
|
// isDeleted/deletedAt/deletedById. Accepting those here would let a
|
|
8
8
|
// softDelete column pass the boot gate and then throw at render-time
|
|
9
9
|
// because the renderer's LIST_ROW_META_COLUMNS doesn't know them.
|
|
10
|
-
const LIST_ROW_META_COLUMN_NAMES = new Set(Object.keys(LIST_ROW_META_COLUMNS));
|
|
10
|
+
export const LIST_ROW_META_COLUMN_NAMES = new Set(Object.keys(LIST_ROW_META_COLUMNS));
|
|
11
11
|
function hasFilterableFields(feature, entityName) {
|
|
12
12
|
const entities = feature.entities;
|
|
13
13
|
if (!entities)
|
|
@@ -60,11 +60,11 @@ export function validateBoot(rawFeatures, options) {
|
|
|
60
60
|
// `…:config:branding-title`; ohne toKebab failt jeder configEdit-Screen mit
|
|
61
61
|
// multi-word Config-Key fälschlich. allConfigKeys oben nutzt das ältere
|
|
62
62
|
// `feature.short`-Format für validateConfigReads.
|
|
63
|
-
const
|
|
63
|
+
const configKeyDefsByQn = new Map();
|
|
64
64
|
for (const f of features) {
|
|
65
|
-
for (const key of Object.
|
|
65
|
+
for (const [key, def] of Object.entries(f.configKeys)) {
|
|
66
66
|
allConfigKeys.add(`${f.name}.${key}`);
|
|
67
|
-
|
|
67
|
+
configKeyDefsByQn.set(qualifyEntityName(f.name, QnTypes.config, key), def);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
// Collect all claim keys — the ownership-rule validator below resolves
|
|
@@ -153,7 +153,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
153
153
|
// Function-Check hier überhaupt liefe.
|
|
154
154
|
validateActionWiring(feature);
|
|
155
155
|
validateFieldWiring(feature);
|
|
156
|
-
validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns,
|
|
156
|
+
validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens);
|
|
157
157
|
validateNavs(feature, allScreenQns, allNavQns, allWorkspaceQns);
|
|
158
158
|
validateWorkspaces(feature, allNavQns);
|
|
159
159
|
}
|
|
@@ -161,7 +161,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
161
161
|
warnOnNavAccessInversion(allNavQns);
|
|
162
162
|
const effectiveNavAllowlist = resolveNavAllowlist(options?.navAllowlist, allNavQns, allWorkspaceQns);
|
|
163
163
|
if (effectiveNavAllowlist !== undefined) {
|
|
164
|
-
warnOnUnreachableNavScreens(allNavQns, effectiveNavAllowlist, options?.navAllowlistExempt);
|
|
164
|
+
warnOnUnreachableNavScreens(allNavQns, effectiveNavAllowlist, options?.navAllowlistExempt, options?.navAllowlist === undefined ? "workspaces" : "explicit");
|
|
165
165
|
}
|
|
166
166
|
validateDefaultWorkspaceUniqueness(allWorkspaceQns);
|
|
167
167
|
validateI18nSurfaceKeys(features);
|
|
@@ -188,7 +188,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
188
188
|
// custom KMS provider) satisfies the requirement without env keys.
|
|
189
189
|
validateEntityFieldEncryptionAvailable(options?.env);
|
|
190
190
|
}
|
|
191
|
-
if (hasFileFields && !process.env[FILE_STORAGE_PROVIDER_ENV]) {
|
|
191
|
+
if (hasFileFields && !(options?.env ?? process.env)[FILE_STORAGE_PROVIDER_ENV]) {
|
|
192
192
|
throw new Error(`${FILE_STORAGE_PROVIDER_ENV} environment variable is required (file/image fields in use)`);
|
|
193
193
|
}
|
|
194
194
|
validateConfigReads(features, allConfigKeys);
|
|
@@ -14,7 +14,8 @@ export declare function validateNavCycles(allNavQns: ReadonlyMap<string, NavDefi
|
|
|
14
14
|
export declare function warnOnNavAccessInversion(allNavQns: ReadonlyMap<string, NavDefinition & {
|
|
15
15
|
readonly featureName: string;
|
|
16
16
|
}>): void;
|
|
17
|
+
export type NavAllowlistSource = "explicit" | "workspaces";
|
|
17
18
|
export declare function warnOnUnreachableNavScreens(allNavQns: ReadonlyMap<string, NavDefinition & {
|
|
18
19
|
readonly featureName: string;
|
|
19
|
-
}>, allowedNavQns: ReadonlySet<string>, navAllowlistExempt?: ReadonlySet<string
|
|
20
|
+
}>, allowedNavQns: ReadonlySet<string>, navAllowlistExempt?: ReadonlySet<string>, allowlistSource?: NavAllowlistSource): void;
|
|
20
21
|
export declare function collectKnownRoles(features: readonly FeatureDefinition[]): Set<string>;
|
|
@@ -125,6 +125,8 @@ export function warnOnNavAccessInversion(allNavQns) {
|
|
|
125
125
|
const leafRoles = navViewerRoles(navDef.access);
|
|
126
126
|
if (parentRoles === undefined || leafRoles === undefined)
|
|
127
127
|
continue;
|
|
128
|
+
if (leafRoles.length === 0)
|
|
129
|
+
continue; // leaf visible to nobody (empty roles or invalid openToAll) — not an inversion
|
|
128
130
|
if (parentRoles.length === 0)
|
|
129
131
|
continue; // parent already visible to nobody — a different problem
|
|
130
132
|
if (leafRoles.some((role) => parentRoles.includes(role)))
|
|
@@ -136,41 +138,29 @@ export function warnOnNavAccessInversion(allNavQns) {
|
|
|
136
138
|
`otherwise align the role sets.`);
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
|
-
|
|
140
|
-
// <id>", see packages/types/src/nav.ts) — this only guards a short id
|
|
141
|
-
// slipping through, since a wrong qualification here would make every nav
|
|
142
|
-
// look orphaned (28 false positives on the real offlot-app schema instead
|
|
143
|
-
// of the intended 8, see fw#3019 measurement).
|
|
144
|
-
function qualifyNavScreenQn(featureName, screen) {
|
|
145
|
-
return screen.includes(":screen:") ? screen : `${featureName}:screen:${screen}`;
|
|
146
|
-
}
|
|
147
|
-
// fw#3019: warn (never throw — a screen deliberately left out of the app's
|
|
148
|
-
// sidebar is legitimate) when a declared nav's screen isn't reachable from
|
|
149
|
-
// any allowlisted nav entry. Checking "nav QN not in allowlist" directly
|
|
150
|
-
// warned 28x on the real offlot-app schema (58 navs, 30 allowed) — most of
|
|
151
|
-
// them app-shell leaves that intentionally re-target a screen another,
|
|
152
|
-
// allowlisted nav already reaches. Reachability warns only for a screen no
|
|
153
|
-
// allowlisted nav points at, which is the actual "unreachable via sidebar"
|
|
154
|
-
// bug (solon#113, offlot's VIN screen in pilot).
|
|
155
|
-
export function warnOnUnreachableNavScreens(allNavQns, allowedNavQns, navAllowlistExempt = new Set()) {
|
|
141
|
+
export function warnOnUnreachableNavScreens(allNavQns, allowedNavQns, navAllowlistExempt = new Set(), allowlistSource = "explicit") {
|
|
156
142
|
const reachableScreenQns = new Set();
|
|
157
143
|
for (const [qn, navDef] of allNavQns) {
|
|
158
144
|
if (!allowedNavQns.has(qn) || navDef.screen === undefined)
|
|
159
145
|
continue;
|
|
160
|
-
reachableScreenQns.add(
|
|
146
|
+
reachableScreenQns.add(navDef.screen);
|
|
161
147
|
}
|
|
162
148
|
for (const [qn, navDef] of allNavQns) {
|
|
163
149
|
if (allowedNavQns.has(qn) || navDef.screen === undefined || navAllowlistExempt.has(qn)) {
|
|
164
150
|
continue;
|
|
165
151
|
}
|
|
166
|
-
|
|
152
|
+
// validateNavs already guarantees a registered, qualified screen QN
|
|
153
|
+
const screenQn = navDef.screen;
|
|
167
154
|
if (reachableScreenQns.has(screenQn))
|
|
168
155
|
continue;
|
|
156
|
+
const remedy = allowlistSource === "workspaces"
|
|
157
|
+
? "add it to a workspace's `nav` array or set `workspaces` on the nav"
|
|
158
|
+
: "add it to the allowlist";
|
|
169
159
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
170
160
|
console.warn(`[kumiko:boot] Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
|
|
171
161
|
`screen "${screenQn}", which no allowlisted nav entry reaches — it is unreachable via ` +
|
|
172
162
|
`the sidebar. If this is intentional (e.g. reachable through a generated hub), add ` +
|
|
173
|
-
`"${qn}" to navAllowlistExempt; otherwise
|
|
163
|
+
`"${qn}" to navAllowlistExempt; otherwise ${remedy}.`);
|
|
174
164
|
}
|
|
175
165
|
}
|
|
176
166
|
// Roles we recognise at boot time. The framework has no explicit
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type OwnershipMap, type WhereRule } from "../ownership.js";
|
|
2
2
|
import type { ClaimKeyDefinition, FeatureDefinition } from "../types/index.js";
|
|
3
3
|
export type WhereRuleProbe = {
|
|
4
4
|
readonly table: unknown;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildEntityTable } from "../../db/table-builder.js";
|
|
2
|
+
import { tableNameOf, } from "../ownership.js";
|
|
2
3
|
import { SYSTEM_USER_ID } from "../system-user.js";
|
|
3
4
|
import { SYSTEM_TENANT_ID } from "../types/identifiers.js";
|
|
4
5
|
import { assertQualifiedWhereFragment, tableColumnSqlNames } from "../where-rule-lint.js";
|
|
@@ -68,7 +69,7 @@ export function validateOwnershipRules(feature, allClaimKeys, knownRoles) {
|
|
|
68
69
|
const table = buildEntityTable(entityName, entity);
|
|
69
70
|
const columns = tableColumnSqlNames(table);
|
|
70
71
|
if (columns.size > 0) {
|
|
71
|
-
probe = { table, tableName:
|
|
72
|
+
probe = { table, tableName: tableNameOf(table), columns };
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
catch {
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import type { EntityDefinition } from "../types/fields.js";
|
|
1
2
|
import type { FeatureDefinition } from "../types/index.js";
|
|
3
|
+
export declare function entityHasAnonymizableSubjectField(fields: EntityDefinition["fields"]): boolean;
|
|
2
4
|
export declare function validatePiiAndRetention(feature: FeatureDefinition): void;
|
|
@@ -17,6 +17,9 @@ const KEEP_FOR_PATTERN = /^\d+[hdwmy]$/;
|
|
|
17
17
|
function hasAnonymizableSubjectField(annot) {
|
|
18
18
|
return Boolean(annot.pii || annot.userOwned || annot.tenantOwned || annot.recordOwned);
|
|
19
19
|
}
|
|
20
|
+
export function entityHasAnonymizableSubjectField(fields) {
|
|
21
|
+
return Object.values(fields).some((f) => hasAnonymizableSubjectField(f));
|
|
22
|
+
}
|
|
20
23
|
function ownerFieldNamesReferencedByPersonalOf(fieldsByName) {
|
|
21
24
|
const ownerFieldNames = new Set();
|
|
22
25
|
for (const field of Object.values(fieldsByName)) {
|
|
@@ -26,6 +29,42 @@ function ownerFieldNamesReferencedByPersonalOf(fieldsByName) {
|
|
|
26
29
|
}
|
|
27
30
|
return ownerFieldNames;
|
|
28
31
|
}
|
|
32
|
+
const WRITE_ONLY_FORBIDDEN_FLAGS = [
|
|
33
|
+
"default",
|
|
34
|
+
"searchable",
|
|
35
|
+
"sortable",
|
|
36
|
+
"filterable",
|
|
37
|
+
"lookupable",
|
|
38
|
+
];
|
|
39
|
+
function validateWriteOnlyField(featureName, entityName, fieldName, field) {
|
|
40
|
+
const where = `[Feature ${featureName}] Field "${fieldName}" on entity "${entityName}"`;
|
|
41
|
+
if (field.type === "embedded") {
|
|
42
|
+
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
43
|
+
if (subField.writeOnly === true) {
|
|
44
|
+
// @cast-boundary schema-walk
|
|
45
|
+
throw new Error(`${where} declares writeOnly on embedded sub-field "${subName}" — writeOnly is only supported on top-level text fields.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// skip: embedded fields carry no writeOnly of their own; sub-fields are checked above
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const flags = field; // @cast-boundary schema-walk
|
|
52
|
+
// skip: only writeOnly fields have constraints to validate
|
|
53
|
+
if (flags.writeOnly !== true)
|
|
54
|
+
return;
|
|
55
|
+
if (field.type !== "text") {
|
|
56
|
+
throw new Error(`${where} declares { writeOnly: true } but has type "${field.type}" — writeOnly only applies to text fields.`);
|
|
57
|
+
}
|
|
58
|
+
if (field.sensitive !== true) {
|
|
59
|
+
throw new Error(`${where} declares { writeOnly: true } without { sensitive: true } — add find: "secret" (ciphertext at rest, stripped from event echoes).`);
|
|
60
|
+
}
|
|
61
|
+
for (const flag of WRITE_ONLY_FORBIDDEN_FLAGS) {
|
|
62
|
+
const value = flags[flag];
|
|
63
|
+
if (value !== undefined && value !== false) {
|
|
64
|
+
throw new Error(`${where} combines { writeOnly: true } with { ${flag} } — a value that is never returned cannot be defaulted, searched, sorted, filtered or looked up.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
29
68
|
// --- PII / Subject-Key Annotations + Retention validation ---
|
|
30
69
|
//
|
|
31
70
|
// Drei Klassen von Checks:
|
|
@@ -92,19 +131,20 @@ export function validatePiiAndRetention(feature) {
|
|
|
92
131
|
sensitiveFlags.encrypted !== true) {
|
|
93
132
|
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares { sensitive: true } without ciphertext-at-rest. Since #967 the event log stores sensitive fields as table ciphertext — add a subject annotation (personal: "self" / "tenant" / { of: "<field>" }) or { encrypted: true }.`);
|
|
94
133
|
}
|
|
134
|
+
validateWriteOnlyField(feature.name, entityName, fieldName, field);
|
|
95
135
|
// Sorting reads the projection column — that stays ciphertext, so
|
|
96
136
|
// sortable + subject annotation stays a boot-fail. searchable has
|
|
97
137
|
// been allowed since #1610: the search consumer decrypts into the
|
|
98
138
|
// derived index and forget purges those docs (see
|
|
99
139
|
// createSearchEventConsumer). sensitive + searchable stays forbidden
|
|
100
|
-
// (
|
|
140
|
+
// (never searchable).
|
|
101
141
|
{
|
|
102
142
|
const flags = field; // @cast-boundary schema-walk
|
|
103
143
|
if (annotCount > 0 && flags.sortable === true) {
|
|
104
144
|
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" combines a subject-key annotation with { sortable: true } — sorting reads the projection column, which is ciphertext at rest. For equality lookups use find: "exact"; drop sortable or keep the field plaintext (personal: false).`);
|
|
105
145
|
}
|
|
106
146
|
if (flags.sensitive === true && flags.searchable === true) {
|
|
107
|
-
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" combines { sensitive: true } with { searchable: true } — sensitive means
|
|
147
|
+
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" combines { sensitive: true } with { searchable: true } — sensitive means ciphertext at rest, never searchable, stripped from event echoes — authorized readers can still read it back (passwords, tokens, tax IDs). Subject-annotated identity fields may be searchable (#1610); sensitive fields may not. To never return the value at all, add { writeOnly: true }.`);
|
|
108
148
|
}
|
|
109
149
|
}
|
|
110
150
|
if (annot.userOwned) {
|
|
@@ -179,12 +219,12 @@ export function validatePiiAndRetention(feature) {
|
|
|
179
219
|
// blockDelete with no field that can carry `anonymize` is fine;
|
|
180
220
|
// subjectRef-only entities are covered by the V3 EXT_USER_DATA guard
|
|
181
221
|
// instead (#1622, #2336).
|
|
182
|
-
const
|
|
222
|
+
const entityHasAnonymizableField = entityHasAnonymizableSubjectField(fieldsByName);
|
|
183
223
|
const hasAnonymize = Object.values(fieldsByName).some((f) => {
|
|
184
224
|
const a = f; // @cast-boundary schema-walk
|
|
185
225
|
return Boolean(a.anonymize);
|
|
186
226
|
});
|
|
187
|
-
if (
|
|
227
|
+
if (entityHasAnonymizableField && !hasAnonymize) {
|
|
188
228
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
189
229
|
console.warn(`[kumiko:boot] [Feature ${feature.name}] Entity "${entityName}" retention.strategy="blockDelete" but no field has an anonymize-function. User-Forget cannot anonymize — Forget will return error. Add { anonymize: () => null } or () => "[ANONYMIZED]" to PII fields.`);
|
|
190
230
|
}
|
|
@@ -88,6 +88,9 @@ const RESERVED_LIST_PAYLOAD_KEYS = new Set([
|
|
|
88
88
|
"filters",
|
|
89
89
|
]);
|
|
90
90
|
function validateProjectionListDateRangeFacets(prefix, screen, schema) {
|
|
91
|
+
// Shared params overwrite each other in the payload, silently dropping the first pick.
|
|
92
|
+
// (Two facets on one field are already rejected by the generic duplicate-facet check.)
|
|
93
|
+
const usedParams = new Set();
|
|
91
94
|
for (const facet of screen.facets ?? []) {
|
|
92
95
|
if (facet.type !== "dateRange")
|
|
93
96
|
continue;
|
|
@@ -100,6 +103,11 @@ function validateProjectionListDateRangeFacets(prefix, screen, schema) {
|
|
|
100
103
|
throw new Error(`${prefix}: dateRange facet on "${facet.field}" names "${param}" as a bound, ` +
|
|
101
104
|
`which is a reserved list-payload key — pick the query's own time-bound param names.`);
|
|
102
105
|
}
|
|
106
|
+
if (usedParams.has(param)) {
|
|
107
|
+
throw new Error(`${prefix}: dateRange facet on "${facet.field}" names "${param}" which another dateRange ` +
|
|
108
|
+
`facet on this screen already uses — the facets would overwrite each other's bound.`);
|
|
109
|
+
}
|
|
110
|
+
usedParams.add(param);
|
|
103
111
|
if (schemaAccepts(schema, param))
|
|
104
112
|
continue;
|
|
105
113
|
throw new Error(`${prefix}: dateRange facet on "${facet.field}" sends "${param}" but query ` +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isPagedQueryHandler } from "../define-handler.js";
|
|
2
|
-
import { normalizeListColumn } from "../screen-helpers.js";
|
|
2
|
+
import { normalizeListColumn, sectionFieldSpecs } from "../screen-helpers.js";
|
|
3
3
|
import { metricField } from "../types/index.js";
|
|
4
4
|
import { buildQueryHandlerMap } from "./projection-list-screens.js";
|
|
5
5
|
import { validateActionFieldRefs } from "./screens.js";
|
|
@@ -37,8 +37,23 @@ function checkPagedHandlerOutputSchemaShape(queryHandlers) {
|
|
|
37
37
|
throw new Error(`Query handler "${qn}" is a paged handler (definePagedQueryHandler) but its outputSchema does not describe the paged envelope { rows: [...], nextCursor, total? } — it has no "rows" field. Did you pass the row schema instead of wrapping it in the envelope?`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
// Usage-site twin of checkPagedHandlerOutputSchemaShape: a plain
|
|
41
|
+
// r.queryHandler/defineQueryHandler feeding a list surface has no paged
|
|
42
|
+
// brand, so declaring the row shape instead of the `{ rows, ... }` envelope
|
|
43
|
+
// would otherwise make every column check below a silent no-op.
|
|
44
|
+
function resolveListRowShape(queryHandlers, queryQn, usage) {
|
|
45
|
+
const outputSchema = queryHandlers.get(queryQn)?.outputSchema;
|
|
46
|
+
const rowShape = getZodRowShape(outputSchema);
|
|
47
|
+
if (rowShape !== undefined)
|
|
48
|
+
return rowShape;
|
|
49
|
+
const envelopeShape = getZodObjectShape(outputSchema);
|
|
50
|
+
// skip: no introspectable object schema (absent / union) — capability absent per module policy
|
|
51
|
+
if (envelopeShape === undefined || "rows" in envelopeShape)
|
|
52
|
+
return undefined;
|
|
53
|
+
throw new Error(`${usage} uses query "${queryQn}" as a list source, but its outputSchema does not describe the paged envelope { rows: [...], nextCursor, total? } — it has no "rows" field. Did you pass the row schema instead of wrapping it in the envelope?`);
|
|
54
|
+
}
|
|
40
55
|
function checkProjectionListOutputColumns(queryHandlers, featureName, screenId, screen) {
|
|
41
|
-
const rowShape =
|
|
56
|
+
const rowShape = resolveListRowShape(queryHandlers, screen.query, `[Feature ${featureName}] Screen "${screenId}" (projectionList)`);
|
|
42
57
|
for (const column of screen.columns) {
|
|
43
58
|
checkColumnField(rowShape, column, (field) => `[Feature ${featureName}] Screen "${screenId}" (projectionList) column "${field}" is not present in query "${screen.query}"'s outputSchema — check for a typo, or add a "label" to mark it a virtual/computed column.`);
|
|
44
59
|
}
|
|
@@ -47,7 +62,7 @@ function checkEditLayoutOutputColumns(queryHandlers, featureName, screenId, scre
|
|
|
47
62
|
for (const section of layout.sections) {
|
|
48
63
|
if (section.kind !== "relatedList")
|
|
49
64
|
continue;
|
|
50
|
-
const rowShape =
|
|
65
|
+
const rowShape = resolveListRowShape(queryHandlers, section.query, `[Feature ${featureName}] Screen "${screenId}" (${screenType}) relatedList section "${section.title}"`);
|
|
51
66
|
for (const column of section.columns) {
|
|
52
67
|
checkColumnField(rowShape, column, (field) => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) relatedList section "${section.title}" column "${field}" is not present in query "${section.query}"'s outputSchema — check for a typo, or add a "label" to mark it a virtual/computed column.`);
|
|
53
68
|
}
|
|
@@ -77,6 +92,9 @@ function checkProjectionDetailOutputFields(queryHandlers, featureName, screenId,
|
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
}
|
|
95
|
+
if (screen.header.subtitleHref !== undefined) {
|
|
96
|
+
checkHeaderField("subtitleHref", screen.header.subtitleHref);
|
|
97
|
+
}
|
|
80
98
|
if (screen.header.status !== undefined)
|
|
81
99
|
checkHeaderField("status", screen.header.status);
|
|
82
100
|
}
|
|
@@ -88,8 +106,26 @@ function checkProjectionDetailOutputFields(queryHandlers, featureName, screenId,
|
|
|
88
106
|
const field = metricField(metric);
|
|
89
107
|
checkFieldExists(recordShape, field, () => `${prefix} metrics references field "${field}" which is not present in query "${screen.query}"'s outputSchema.`);
|
|
90
108
|
}
|
|
109
|
+
checkDetailSectionFields(recordShape, prefix, screen);
|
|
91
110
|
checkRelatedListToolbarActionFields(recordShape, featureName, screenId, screen);
|
|
92
111
|
}
|
|
112
|
+
// Field-grid sections read the enclosing record (the query result), so a typo
|
|
113
|
+
// is a silently-empty cell. writeForm sections are skipped on purpose: their
|
|
114
|
+
// fields are form inputs defined by `fieldDefs`, not necessarily record keys.
|
|
115
|
+
function checkDetailSectionFields(recordShape, prefix, screen) {
|
|
116
|
+
for (const section of screen.layout.sections) {
|
|
117
|
+
const countField = "countField" in section ? section.countField : undefined;
|
|
118
|
+
if (countField !== undefined) {
|
|
119
|
+
checkFieldExists(recordShape, countField, () => `${prefix} section "${section.title ?? "(untitled)"}" countField references field "${countField}" which is not present in query "${screen.query}"'s outputSchema.`);
|
|
120
|
+
}
|
|
121
|
+
if (section.kind !== undefined && section.kind !== "fields")
|
|
122
|
+
continue;
|
|
123
|
+
for (const entry of sectionFieldSpecs(section)) {
|
|
124
|
+
const field = typeof entry === "string" ? entry : entry.field;
|
|
125
|
+
checkFieldExists(recordShape, field, () => `${prefix} section "${section.title ?? section.id ?? "(untitled)"}" references field "${field}" which is not present in query "${screen.query}"'s outputSchema.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
93
129
|
// relatedList toolbarActions' visible/params evaluate against the ENCLOSING
|
|
94
130
|
// projectionDetail's record, not the relatedList's own row — reuses the
|
|
95
131
|
// same per-action field-ref check as rowActions/toolbarActions elsewhere
|
|
@@ -139,7 +175,7 @@ function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen
|
|
|
139
175
|
}
|
|
140
176
|
}
|
|
141
177
|
else if (panel.kind === "list") {
|
|
142
|
-
const rowShape =
|
|
178
|
+
const rowShape = resolveListRowShape(queryHandlers, panel.query, `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}"`);
|
|
143
179
|
for (const column of panel.columns) {
|
|
144
180
|
checkColumnField(rowShape, column, (field) => `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" column "${field}" is not present in query "${panel.query}"'s outputSchema — check for a typo, or add a "label" to mark it a virtual/computed column.`);
|
|
145
181
|
}
|
|
@@ -154,7 +190,7 @@ function checkScreenOutputColumns(queryHandlers, featureName, screenId, screen)
|
|
|
154
190
|
}
|
|
155
191
|
else if (screen.type === "entityList") {
|
|
156
192
|
if (screen.expandableRow !== undefined) {
|
|
157
|
-
const rowShape =
|
|
193
|
+
const rowShape = resolveListRowShape(queryHandlers, screen.expandableRow.query, `[Feature ${featureName}] Screen "${screenId}" (entityList) expandableRow "${screen.expandableRow.title}"`);
|
|
158
194
|
for (const column of screen.expandableRow.columns) {
|
|
159
195
|
checkColumnField(rowShape, column, (field) => `[Feature ${featureName}] Screen "${screenId}" (entityList) expandableRow "${screen.expandableRow?.title}" column "${field}" is not present in query "${screen.expandableRow?.query}"'s outputSchema — check for a typo, or add a "label" to mark it a virtual/computed column.`);
|
|
160
196
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IconKey } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
2
|
-
import type { FeatureDefinition } from "../types/index.js";
|
|
2
|
+
import type { ConfigKeyDefinition, FeatureDefinition } from "../types/index.js";
|
|
3
3
|
import type { DashboardCustomPanel, DashboardScreenPanel, RelatedListToolbarAction, RowAction, RowFieldExtractor, ScreenDefinition, ToolbarAction } from "../types/screen.js";
|
|
4
4
|
export declare function rowFieldExtractorKeys(params: RowFieldExtractor): readonly string[];
|
|
5
5
|
export declare function readsNavigateParamsAsFormPrefill(target: ScreenDefinition, explicitEntityId: string | undefined, sourceScreenEntity: string | undefined): boolean;
|
|
@@ -13,7 +13,7 @@ export declare function validateScreenShortIdCollisions(screensByShortId: Readon
|
|
|
13
13
|
readonly screen: ScreenDefinition;
|
|
14
14
|
}>>): void;
|
|
15
15
|
export declare function resolveScreenTargetQn(featureName: string, target: string): string;
|
|
16
|
-
export declare function validateScreens(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, allWriteHandlerQns: ReadonlySet<string>, allScreenQns: ReadonlySet<string>,
|
|
16
|
+
export declare function validateScreens(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, allWriteHandlerQns: ReadonlySet<string>, allScreenQns: ReadonlySet<string>, configKeyDefsByQn: ReadonlyMap<string, ConfigKeyDefinition>, screensByShortId: ReadonlyMap<string, ReadonlyArray<{
|
|
17
17
|
readonly featureName: string;
|
|
18
18
|
readonly screen: ScreenDefinition;
|
|
19
19
|
}>>, detailForScreens: ReadonlyMap<string, {
|