@cosmicdrift/kumiko-framework 0.398.0 → 0.400.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/AGENTS.md +33 -19
  2. package/agents/api/dev-server-testing.md +23 -0
  3. package/agents/api/framework-db.md +277 -0
  4. package/agents/api/framework-engine.md +1810 -0
  5. package/agents/api/framework-errors.md +119 -0
  6. package/agents/api/framework-event-store.md +99 -0
  7. package/agents/api/framework-seeding.md +26 -0
  8. package/agents/api/framework-ui-types.md +542 -0
  9. package/agents/api/renderer-web.md +1450 -0
  10. package/agents/api/testing-helpers.md +189 -0
  11. package/agents/api/testing-stack.md +144 -0
  12. package/agents/api/types.md +5124 -0
  13. package/agents/api.md +20 -0
  14. package/agents/examples/derived-fields-sql.md +251 -0
  15. package/agents/examples/documents.md +161 -0
  16. package/agents/examples/inspection-checklist.md +807 -0
  17. package/agents/examples.md +3 -0
  18. package/agents/features-index.md +178 -0
  19. package/agents/features.md +9 -1
  20. package/dist/api/entity-read-access.d.ts +2 -0
  21. package/dist/api/entity-read-access.js +46 -2
  22. package/dist/api/server-boot-checks.js +2 -2
  23. package/dist/api/server-routes.js +3 -1
  24. package/dist/db/derived-field-sql.d.ts +23 -0
  25. package/dist/db/derived-field-sql.js +115 -0
  26. package/dist/db/derived-related-sql.d.ts +31 -0
  27. package/dist/db/derived-related-sql.js +137 -0
  28. package/dist/db/driver-row-coercion.d.ts +1 -0
  29. package/dist/db/driver-row-coercion.js +13 -0
  30. package/dist/db/eagerload.d.ts +3 -1
  31. package/dist/db/eagerload.js +4 -6
  32. package/dist/db/event-store-executor-context.js +1 -1
  33. package/dist/db/event-store-executor-read-paging.js +1 -1
  34. package/dist/db/event-store-executor-read-search.d.ts +3 -0
  35. package/dist/db/event-store-executor-read-search.js +17 -9
  36. package/dist/db/event-store-executor-read-sort.d.ts +9 -2
  37. package/dist/db/event-store-executor-read-sort.js +27 -14
  38. package/dist/db/event-store-executor-read-where.d.ts +0 -5
  39. package/dist/db/event-store-executor-read-where.js +3 -9
  40. package/dist/db/event-store-executor-read.js +23 -4
  41. package/dist/db/event-store-executor-write-steps.d.ts +6 -1
  42. package/dist/db/event-store-executor-write-steps.js +20 -1
  43. package/dist/db/event-store-executor-write.js +8 -8
  44. package/dist/db/money.d.ts +4 -0
  45. package/dist/db/money.js +34 -29
  46. package/dist/db/parent-ref-clause.d.ts +1 -0
  47. package/dist/db/parent-ref-clause.js +18 -8
  48. package/dist/db/read-gate-clause.d.ts +7 -0
  49. package/dist/db/read-gate-clause.js +188 -0
  50. package/dist/db/read-ownership.d.ts +9 -0
  51. package/dist/db/read-ownership.js +12 -0
  52. package/dist/engine/boot-validator/boot-check.d.ts +3 -1
  53. package/dist/engine/boot-validator/boot-check.js +7 -11
  54. package/dist/engine/boot-validator/boot-context.d.ts +26 -0
  55. package/dist/engine/boot-validator/boot-context.js +57 -0
  56. package/dist/engine/boot-validator/boot-options.d.ts +33 -0
  57. package/dist/engine/boot-validator/boot-options.js +1 -0
  58. package/dist/engine/boot-validator/boot-phases-app.d.ts +5 -0
  59. package/dist/engine/boot-validator/boot-phases-app.js +103 -0
  60. package/dist/engine/boot-validator/boot-phases.d.ts +11 -0
  61. package/dist/engine/boot-validator/boot-phases.js +140 -0
  62. package/dist/engine/boot-validator/boot-report.d.ts +6 -0
  63. package/dist/engine/boot-validator/boot-report.js +47 -0
  64. package/dist/engine/boot-validator/boot-rule.d.ts +12 -0
  65. package/dist/engine/boot-validator/boot-rule.js +14 -0
  66. package/dist/engine/boot-validator/boot-warnings.d.ts +2 -0
  67. package/dist/engine/boot-validator/boot-warnings.js +20 -0
  68. package/dist/engine/boot-validator/derived-sql-fields.d.ts +4 -0
  69. package/dist/engine/boot-validator/derived-sql-fields.js +184 -0
  70. package/dist/engine/boot-validator/entity-handler.d.ts +3 -0
  71. package/dist/engine/boot-validator/entity-handler.js +6 -3
  72. package/dist/engine/boot-validator/entity-list-screens.js +3 -1
  73. package/dist/engine/boot-validator/field-conditions.d.ts +2 -0
  74. package/dist/engine/boot-validator/field-conditions.js +178 -0
  75. package/dist/engine/boot-validator/i18n-keys.d.ts +2 -2
  76. package/dist/engine/boot-validator/i18n-keys.js +1 -3
  77. package/dist/engine/boot-validator/index.d.ts +5 -34
  78. package/dist/engine/boot-validator/index.js +24 -204
  79. package/dist/engine/boot-validator/pii-retention.js +2 -7
  80. package/dist/engine/boot-validator/projection-list-screens.js +19 -1
  81. package/dist/engine/boot-validator/read-gate.d.ts +2 -0
  82. package/dist/engine/boot-validator/read-gate.js +54 -0
  83. package/dist/engine/boot-validator/screens-forms.js +19 -6
  84. package/dist/engine/boot-validator/screens-kind-entity-list.js +8 -6
  85. package/dist/engine/boot-validator/screens-lists.js +8 -0
  86. package/dist/engine/boot-validator/zod-shape.js +4 -2
  87. package/dist/engine/build-app-schema.js +11 -5
  88. package/dist/engine/embedded-derived.d.ts +8 -6
  89. package/dist/engine/embedded-derived.js +25 -7
  90. package/dist/engine/entity-handlers.d.ts +1 -1
  91. package/dist/engine/entity-handlers.js +17 -18
  92. package/dist/engine/entity-list-runtime-options.d.ts +5 -1
  93. package/dist/engine/entity-list-runtime-options.js +103 -4
  94. package/dist/engine/factories.d.ts +5 -5
  95. package/dist/engine/factories.js +3 -5
  96. package/dist/engine/field-access.d.ts +2 -0
  97. package/dist/engine/field-access.js +48 -1
  98. package/dist/engine/field-state.d.ts +12 -0
  99. package/dist/engine/field-state.js +67 -0
  100. package/dist/engine/handler-helpers.d.ts +3 -0
  101. package/dist/engine/handler-helpers.js +7 -1
  102. package/dist/engine/index.d.ts +2 -1
  103. package/dist/engine/index.js +2 -1
  104. package/dist/engine/ownership.d.ts +1 -1
  105. package/dist/engine/ownership.js +9 -5
  106. package/dist/engine/registry-state.d.ts +1 -0
  107. package/dist/engine/registry-state.js +1 -1
  108. package/dist/engine/schema-builder.js +3 -0
  109. package/dist/engine/strip-unreadable-related-lists.d.ts +4 -0
  110. package/dist/engine/strip-unreadable-related-lists.js +80 -0
  111. package/dist/entrypoint/index.js +1 -0
  112. package/dist/entrypoint/internals.d.ts +2 -0
  113. package/dist/files/assert-file-refs-belong-to-tenant.js +26 -6
  114. package/dist/files/bind-pending-file-refs.d.ts +3 -2
  115. package/dist/files/bind-pending-file-refs.js +11 -12
  116. package/dist/files/bind-referenced-files.write.js +5 -5
  117. package/dist/files/create-file-ref-from-bytes.d.ts +2 -0
  118. package/dist/files/create-file-ref-from-bytes.js +2 -0
  119. package/dist/files/create-file-ref.d.ts +2 -0
  120. package/dist/files/create-file-ref.js +39 -22
  121. package/dist/files/dispose-removed-file-refs.js +5 -8
  122. package/dist/files/file-field-slots.d.ts +10 -0
  123. package/dist/files/file-field-slots.js +53 -0
  124. package/dist/files/file-ref-entity.d.ts +1 -1
  125. package/dist/files/file-ref-table.d.ts +1 -1
  126. package/dist/files/file-routes.js +30 -9
  127. package/dist/files/index.d.ts +1 -0
  128. package/dist/files/index.js +1 -0
  129. package/dist/files/load-save-file-refs.js +3 -4
  130. package/dist/files/record-read-check.js +2 -30
  131. package/dist/files/record-write-check.d.ts +2 -0
  132. package/dist/files/record-write-check.js +18 -3
  133. package/dist/files/upload-field-resolution.d.ts +4 -1
  134. package/dist/files/upload-field-resolution.js +57 -12
  135. package/dist/pipeline/dispatch-batch.js +6 -5
  136. package/dist/pipeline/dispatch-write.js +2 -1
  137. package/dist/pipeline/write-result-masking.js +2 -1
  138. package/dist/schema-cli/validate-command.js +46 -12
  139. package/dist/schema-cli.js +1 -1
  140. package/dist/ui-types/index.d.ts +2 -1
  141. package/dist/ui-types/index.js +2 -1
  142. package/package.json +3 -3
  143. package/src/changes.json +67 -0
@@ -0,0 +1,33 @@
1
+ import type { EnvelopeCipher } from "../../secrets/envelope-cipher.ts";
2
+ export type ValidateBootOptions = {
3
+ /** Warn when an access role is used by exactly one handler/config-key/
4
+ * field across the whole boot scan — often a typo, but also the normal
5
+ * shape of a legitimate fine-grained role (one role scoped to one admin
6
+ * endpoint on purpose). Opt-in (default false): a default-on
7
+ * prod warning that nobody can silence per-role isn't worth the noise
8
+ * it generates on every boot. */
9
+ readonly warnOnUniqueAccessRoles?: boolean;
10
+ /** QNs the app's sidebar allowlist admits. When set, every declared nav
11
+ * whose screen isn't reachable from any allowlisted nav entry gets a
12
+ * boot-time warning — that screen would otherwise be silently
13
+ * unreachable via the sidebar. When omitted and the app has
14
+ * workspaces, the allowlist is derived from `r.workspace({ nav })` and
15
+ * `r.nav({ workspaces })` assignments instead. */
16
+ readonly navAllowlist?: ReadonlySet<string>;
17
+ /** Nav QNs deliberately left out of the sidebar allowlist because their
18
+ * screen is reachable another way (e.g. as a sub-page of a generated
19
+ * settings hub). Suppresses the navAllowlist warning for these QNs. */
20
+ readonly navAllowlistExempt?: ReadonlySet<string>;
21
+ /** Env the encrypted-field master-key probe reads. Defaults to
22
+ * `process.env`; runners pass their resolved env so a ciphertext-only
23
+ * key (already decrypted at boot) is visible to the probe. */
24
+ readonly env?: Readonly<Record<string, string | undefined>>;
25
+ /** The cipher the runner injects after validation (built from the app's
26
+ * `masterKey` or the env keyring). Satisfies the availability check for
27
+ * `encrypted: true` fields without an env KEK. */
28
+ readonly entityFieldCipher?: EnvelopeCipher;
29
+ /** Locales every translation key must carry across all mounted features
30
+ * (locale packages count). Without it a key can ship in one language and
31
+ * silently fall back at runtime for users of the others. */
32
+ readonly requiredLocales?: readonly string[];
33
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { BootContext } from "./boot-context.ts";
2
+ import type { FeatureFlags } from "./boot-phases.ts";
3
+ import type { BootReport } from "./boot-report.ts";
4
+ export declare function runAppWidePhase(report: BootReport, ctx: BootContext): void;
5
+ export declare function runEnvironmentPhase(report: BootReport, ctx: BootContext, flags: FeatureFlags): void;
@@ -0,0 +1,103 @@
1
+ import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-encryption.js";
2
+ import { FILE_STORAGE_PROVIDER_ENV } from "../extension-names.js";
3
+ import { createRegistry } from "../registry.js";
4
+ import { validateProjectionAllowlist } from "../validate-projection-allowlist.js";
5
+ import { runFeatureBootCheck } from "./boot-check.js";
6
+ import { validateCardMetaOverflow } from "./card-meta-overflow.js";
7
+ import { validateConfigReads } from "./config-deps.js";
8
+ import { validateExtensionPreSaveWiring } from "./entity-handler.js";
9
+ import { validateEntityListScreens } from "./entity-list-screens.js";
10
+ import { validateGdprStoragePersistence } from "./gdpr-storage.js";
11
+ import { validateGlobalTenancyEntities } from "./global-tenancy.js";
12
+ import { validateI18nSurfaceKeys, validateRequiredLocales, validateTranslationLocaleConsistency, } from "./i18n-keys.js";
13
+ import { validateNavCycles } from "./nav.js";
14
+ import { validateProjectionListScreens, validateRelatedListSectionQueries, } from "./projection-list-screens.js";
15
+ import { validateQueryOutputColumns } from "./query-output-columns.js";
16
+ import { validateExtensionSelectorPanels } from "./selector-owner-panels.js";
17
+ import { validateSubmitPrefilledHiddenFields } from "./url-prefill-fields.js";
18
+ import { validateDefaultWorkspaceUniqueness } from "./workspaces.js";
19
+ const APP_CHECKS_AFTER_I18N = [
20
+ {
21
+ check: "validateTranslationLocaleConsistency",
22
+ run: (c) => validateTranslationLocaleConsistency(c.features),
23
+ },
24
+ {
25
+ check: "validateRequiredLocales",
26
+ run: (c) => validateRequiredLocales(c.features, c.options?.requiredLocales),
27
+ },
28
+ { check: "validateEntityListScreens", run: (c) => validateEntityListScreens(c.features) },
29
+ {
30
+ check: "validateSubmitPrefilledHiddenFields",
31
+ run: (c) => validateSubmitPrefilledHiddenFields(c.features),
32
+ },
33
+ { check: "validateCardMetaOverflow", run: (c) => validateCardMetaOverflow(c.features) },
34
+ {
35
+ check: "validateExtensionSelectorPanels",
36
+ run: (c) => validateExtensionSelectorPanels(c.features),
37
+ },
38
+ // Column/field checks assume every `query` string already resolves (validateQueryRefs, phase 1).
39
+ { check: "validateQueryOutputColumns", run: (c) => validateQueryOutputColumns(c.features) },
40
+ {
41
+ check: "validateProjectionListScreens",
42
+ run: (c) => validateProjectionListScreens(c.features),
43
+ },
44
+ {
45
+ check: "validateRelatedListSectionQueries",
46
+ run: (c) => validateRelatedListSectionQueries(c.features),
47
+ },
48
+ {
49
+ check: "validateExtensionPreSaveWiring",
50
+ run: (c) => validateExtensionPreSaveWiring(c.features),
51
+ },
52
+ {
53
+ check: "validateGdprStoragePersistence",
54
+ run: (c) => validateGdprStoragePersistence(c.features),
55
+ },
56
+ {
57
+ check: "validateGlobalTenancyEntities",
58
+ run: (c) => validateGlobalTenancyEntities(c.features),
59
+ },
60
+ ];
61
+ function runFeatureBootChecks(report, ctx) {
62
+ for (const feature of ctx.features) {
63
+ for (const check of feature.bootChecks) {
64
+ report.check("runFeatureBootCheck", feature.name, () => runFeatureBootCheck(feature, check, ctx.features));
65
+ }
66
+ }
67
+ }
68
+ export function runAppWidePhase(report, ctx) {
69
+ const { features } = ctx;
70
+ report.check("validateNavCycles", undefined, () => validateNavCycles(ctx.allNavQns));
71
+ report.check("validateDefaultWorkspaceUniqueness", undefined, () => validateDefaultWorkspaceUniqueness(ctx.allWorkspaceQns));
72
+ const built = {};
73
+ report.check("createRegistry", undefined, () => {
74
+ built.registry = createRegistry(features);
75
+ });
76
+ const registry = built.registry;
77
+ if (registry !== undefined) {
78
+ report.check("validateI18nSurfaceKeys", undefined, () => validateI18nSurfaceKeys(features, registry));
79
+ }
80
+ for (const { check, run } of APP_CHECKS_AFTER_I18N) {
81
+ report.check(check, undefined, () => run(ctx));
82
+ }
83
+ runFeatureBootChecks(report, ctx);
84
+ report.check("validateProjectionAllowlist", undefined, () => validateProjectionAllowlist(features));
85
+ report.check("validateConfigReads", undefined, () => validateConfigReads(features, ctx.allConfigKeys));
86
+ }
87
+ // Independent of the earlier phases: whether the environment can serve the app is
88
+ // a separate question from whether its definitions are consistent.
89
+ export function runEnvironmentPhase(report, ctx, flags) {
90
+ const { options } = ctx;
91
+ if (flags.hasEncryptedFields) {
92
+ // Availability check, not env-presence: eagerly building the keyring
93
+ // catches malformed keys (wrong length, bad base64) at boot instead of
94
+ // on the first encrypted read in prod. An injected cipher (test seam,
95
+ // custom KMS provider) satisfies the requirement without env keys.
96
+ report.check("validateEntityFieldEncryptionAvailable", undefined, () => validateEntityFieldEncryptionAvailable(options?.env, options?.entityFieldCipher));
97
+ }
98
+ report.check("fileStorageProvider", undefined, () => {
99
+ if (flags.hasFileFields && !(options?.env ?? process.env)[FILE_STORAGE_PROVIDER_ENV]) {
100
+ throw new Error(`${FILE_STORAGE_PROVIDER_ENV} environment variable is required (file/image fields in use)`);
101
+ }
102
+ });
103
+ }
@@ -0,0 +1,11 @@
1
+ import type { BootContext } from "./boot-context.ts";
2
+ import type { BootReport } from "./boot-report.ts";
3
+ import { collectDetailForScreens } from "./detail-screens.ts";
4
+ export type FeatureFlags = {
5
+ hasEncryptedFields: boolean;
6
+ hasFileFields: boolean;
7
+ };
8
+ type DetailForScreens = ReturnType<typeof collectDetailForScreens>;
9
+ export declare function runStructurePhase(report: BootReport, ctx: BootContext): DetailForScreens;
10
+ export declare function runFeaturePhase(report: BootReport, ctx: BootContext, detailForScreens: DetailForScreens): FeatureFlags;
11
+ export {};
@@ -0,0 +1,140 @@
1
+ import { validateAccessDeclarations } from "./access-declarations.js";
2
+ import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
3
+ import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext.js";
4
+ import { collectExposedApis } from "./boot-context.js";
5
+ import { validateCircularDeps, validateConfigKeyAllowPerRequest, validateConfigKeyBacking, validateConfigKeyBounds, validateConfigKeyComputed, validateConfigKeyOptionsQuery, validateConfigKeyPiiEncrypted, validateConfigKeyRequired, } from "./config-deps.js";
6
+ import { validateSqlDerivedFields } from "./derived-sql-fields.js";
7
+ import { collectDetailForScreens } from "./detail-screens.js";
8
+ import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectAvailabilityQuery, validateEntitySelectConditionalOptions, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateLockedWhen, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateSearchableFieldReadAccess, validateTransitions, } from "./entity-handler.js";
9
+ import { validateFieldConditions } from "./field-conditions.js";
10
+ import { validateHandlerFieldReferences } from "./handler-field-references.js";
11
+ import { validateNavs } from "./nav.js";
12
+ import { validateOwnershipRules } from "./ownership.js";
13
+ import { validateParentRefs } from "./parent-ref.js";
14
+ import { validatePiiAndRetention } from "./pii-retention.js";
15
+ import { validateQueryRefs } from "./query-refs.js";
16
+ import { validateReadGates } from "./read-gate.js";
17
+ import { validateRecordOwnedSubjects } from "./record-owned.js";
18
+ import { validateScreenShortIdCollisions, validateScreens } from "./screens.js";
19
+ import { validateTransferGraph } from "./transfer-graph.js";
20
+ import { validateWorkspaces } from "./workspaces.js";
21
+ function runFeatureChecks(report, checks, feature, ctx) {
22
+ for (const { check, run } of checks) {
23
+ report.check(check, feature.name, () => run(feature, ctx));
24
+ }
25
+ }
26
+ // Phase 1 breaks the shared lookup tables later checks resolve against; if it
27
+ // finds anything, the later phases would only report follow-up noise.
28
+ const STRUCTURE_FEATURE_CHECKS = [
29
+ { check: "validateCircularDeps", run: (f, c) => validateCircularDeps(f.name, c.featureMap) },
30
+ {
31
+ check: "validateExtensionUsages",
32
+ run: (f, c) => validateExtensionUsages(f, c.extensionProviders),
33
+ },
34
+ {
35
+ check: "validateReferenceFields",
36
+ run: (f, c) => validateReferenceFields(f, c.featureMap, c.queryHandlerQns),
37
+ },
38
+ { check: "validateParentRefs", run: (f, c) => validateParentRefs(f, c.featureMap) },
39
+ ];
40
+ export function runStructurePhase(report, ctx) {
41
+ const { features, featureMap } = ctx;
42
+ const built = {};
43
+ report.check("validateScreenShortIdCollisions", undefined, () => validateScreenShortIdCollisions(ctx.screensByShortId));
44
+ report.check("collectDetailForScreens", undefined, () => {
45
+ built.detailForScreens = collectDetailForScreens(features, featureMap);
46
+ });
47
+ report.check("validateSqlDerivedFields", undefined, () => validateSqlDerivedFields(features));
48
+ report.check("collectExposedApis", undefined, () => {
49
+ built.exposedApis = collectExposedApis(features);
50
+ });
51
+ report.check("validateQueryRefs", undefined, () => validateQueryRefs(features));
52
+ for (const feature of features) {
53
+ // Without a unique API map the matching would report every API as missing.
54
+ const apis = built.exposedApis;
55
+ if (apis !== undefined) {
56
+ report.check("validateApiExposureMatching", feature.name, () => validateApiExposureMatching(feature, apis, featureMap));
57
+ }
58
+ runFeatureChecks(report, STRUCTURE_FEATURE_CHECKS, feature, ctx);
59
+ }
60
+ report.endPhase();
61
+ return built.detailForScreens ?? new Map();
62
+ }
63
+ const FEATURE_CHECKS = [
64
+ { check: "validateSearchableFieldReadAccess", run: (f) => validateSearchableFieldReadAccess(f) },
65
+ { check: "validatePiiAndRetention", run: (f) => validatePiiAndRetention(f) },
66
+ { check: "validateRecordOwnedSubjects", run: (f) => validateRecordOwnedSubjects(f) },
67
+ { check: "validateEntitySelectOptionsQuery", run: (f) => validateEntitySelectOptionsQuery(f) },
68
+ {
69
+ check: "validateEntitySelectAvailabilityQuery",
70
+ run: (f, c) => validateEntitySelectAvailabilityQuery(f, c.queryHandlerQns),
71
+ },
72
+ {
73
+ check: "validateEntitySelectConditionalOptions",
74
+ run: (f) => validateEntitySelectConditionalOptions(f),
75
+ },
76
+ {
77
+ check: "validateEmbeddedFields",
78
+ run: (f, c) => validateEmbeddedFields(f, c.featureMap, c.queryHandlerQns),
79
+ },
80
+ { check: "validateMultiSelectFields", run: (f) => validateMultiSelectFields(f) },
81
+ { check: "validateImageVariants", run: (f) => validateImageVariants(f) },
82
+ {
83
+ check: "validateHandlerFieldReferences",
84
+ run: (f, c) => validateHandlerFieldReferences(f, c.featureMap),
85
+ },
86
+ { check: "validateTransitions", run: (f) => validateTransitions(f) },
87
+ { check: "validateLockedWhen", run: (f) => validateLockedWhen(f) },
88
+ { check: "validateFieldConditions", run: (f) => validateFieldConditions(f) },
89
+ { check: "validateExtendSchemaCollisions", run: (f) => validateExtendSchemaCollisions(f) },
90
+ { check: "validateDerivedFieldCollisions", run: (f) => validateDerivedFieldCollisions(f) },
91
+ { check: "validateHandlerAccess", run: (f) => validateHandlerAccess(f) },
92
+ { check: "validateAccessDeclarations", run: (f) => validateAccessDeclarations(f) },
93
+ { check: "validateLocatedTimestamps", run: (f) => validateLocatedTimestamps(f) },
94
+ { check: "validateEntityIndexes", run: (f) => validateEntityIndexes(f) },
95
+ { check: "validateConfigKeyBounds", run: (f) => validateConfigKeyBounds(f) },
96
+ { check: "validateConfigKeyRequired", run: (f) => validateConfigKeyRequired(f) },
97
+ { check: "validateConfigKeyComputed", run: (f) => validateConfigKeyComputed(f) },
98
+ { check: "validateConfigKeyAllowPerRequest", run: (f) => validateConfigKeyAllowPerRequest(f) },
99
+ { check: "validateConfigKeyBacking", run: (f) => validateConfigKeyBacking(f) },
100
+ { check: "validateConfigKeyOptionsQuery", run: (f) => validateConfigKeyOptionsQuery(f) },
101
+ { check: "validateConfigKeyPiiEncrypted", run: (f) => validateConfigKeyPiiEncrypted(f) },
102
+ {
103
+ check: "validateOwnershipRules",
104
+ run: (f, c) => validateOwnershipRules(f, c.allClaimKeys, c.knownRoles),
105
+ },
106
+ { check: "validateReadGates", run: (f, c) => validateReadGates(f, c.featureMap) },
107
+ { check: "validateTransferGraph", run: (f, c) => validateTransferGraph(f, c.featureMap) },
108
+ { check: "validateMultiStreamProjections", run: (f) => validateMultiStreamProjections(f) },
109
+ ];
110
+ function validateScreensOfFeature(feature, ctx, detailForScreens) {
111
+ validateScreens(feature, ctx.featureMap, ctx.allWriteHandlerQns, ctx.allScreenQns, ctx.configKeyDefsByQn, ctx.screensByShortId, detailForScreens);
112
+ }
113
+ // validateScreens flags a Function-valued visible/entityId as "unknown field
114
+ // undefined" before the clear wiring message could surface, so a wiring
115
+ // violation in a feature suppresses its screens check.
116
+ function runWiringAndScreens(report, feature, ctx, detailForScreens) {
117
+ const actionWiringOk = report.check("validateActionWiring", feature.name, () => validateActionWiring(feature));
118
+ const fieldWiringOk = report.check("validateFieldWiring", feature.name, () => validateFieldWiring(feature));
119
+ if (actionWiringOk && fieldWiringOk) {
120
+ report.check("validateScreens", feature.name, () => validateScreensOfFeature(feature, ctx, detailForScreens));
121
+ }
122
+ report.check("validateNavs", feature.name, () => validateNavs(feature, ctx.allScreenQns, ctx.allNavQns, ctx.allWorkspaceQns));
123
+ report.check("validateWorkspaces", feature.name, () => validateWorkspaces(feature, ctx.allNavQns));
124
+ }
125
+ export function runFeaturePhase(report, ctx, detailForScreens) {
126
+ const flags = { hasEncryptedFields: false, hasFileFields: false };
127
+ for (const feature of ctx.features) {
128
+ report.check("validateEncryptedFields", feature.name, () => {
129
+ if (validateEncryptedFields(feature))
130
+ flags.hasEncryptedFields = true;
131
+ });
132
+ report.check("validateFileFields", feature.name, () => {
133
+ if (validateFileFields(feature))
134
+ flags.hasFileFields = true;
135
+ });
136
+ runFeatureChecks(report, FEATURE_CHECKS, feature, ctx);
137
+ runWiringAndScreens(report, feature, ctx, detailForScreens);
138
+ }
139
+ return flags;
140
+ }
@@ -0,0 +1,6 @@
1
+ export type BootReport = {
2
+ check(checkName: string, feature: string | undefined, fn: () => unknown): boolean;
3
+ endPhase(): void;
4
+ finish(): void;
5
+ };
6
+ export declare function createBootReport(): BootReport;
@@ -0,0 +1,47 @@
1
+ import { BootValidationError } from "./boot-rule.js";
2
+ // A runtime crash is a bug in a validator (or in the input it assumed), not a
3
+ // rule violation: it must surface as itself instead of masquerading as a finding.
4
+ function isRuntimeCrash(thrown) {
5
+ return (!(thrown instanceof Error) ||
6
+ thrown instanceof TypeError ||
7
+ thrown instanceof RangeError ||
8
+ thrown instanceof ReferenceError ||
9
+ thrown instanceof SyntaxError ||
10
+ thrown instanceof EvalError ||
11
+ thrown instanceof URIError);
12
+ }
13
+ export function createBootReport() {
14
+ const violations = [];
15
+ let firstCrash;
16
+ function endPhase() {
17
+ // Crashes are discarded next to violations: they are most likely follow-ups of the broken input.
18
+ if (violations.length > 0)
19
+ throw new BootValidationError([...violations]);
20
+ if (firstCrash !== undefined)
21
+ throw firstCrash.thrown;
22
+ }
23
+ return {
24
+ check(checkName, feature, fn) {
25
+ try {
26
+ fn();
27
+ return true;
28
+ }
29
+ catch (thrown) {
30
+ if (isRuntimeCrash(thrown)) {
31
+ firstCrash ??= { thrown };
32
+ }
33
+ else if (thrown instanceof Error) {
34
+ violations.push({
35
+ ruleId: `unclassified/${checkName}`,
36
+ check: checkName,
37
+ ...(feature === undefined ? {} : { feature }),
38
+ message: thrown.message,
39
+ });
40
+ }
41
+ return false;
42
+ }
43
+ },
44
+ endPhase,
45
+ finish: endPhase,
46
+ };
47
+ }
@@ -0,0 +1,12 @@
1
+ export type BootViolation = {
2
+ readonly ruleId: string;
3
+ readonly check: string;
4
+ readonly feature?: string;
5
+ readonly message: string;
6
+ readonly fix?: string;
7
+ };
8
+ export declare class BootValidationError extends Error {
9
+ readonly violations: readonly BootViolation[];
10
+ readonly name = "BootValidationError";
11
+ constructor(violations: readonly BootViolation[]);
12
+ }
@@ -0,0 +1,14 @@
1
+ function describeViolation(violation) {
2
+ const line = `- [${violation.ruleId}] ${violation.message}`;
3
+ return violation.fix === undefined ? line : `${line}\n Fix: ${violation.fix}`;
4
+ }
5
+ export class BootValidationError extends Error {
6
+ violations;
7
+ name = "BootValidationError";
8
+ constructor(violations) {
9
+ const count = violations.length;
10
+ const header = `Boot validation failed: ${count} violation${count === 1 ? "" : "s"}`;
11
+ super([header, ...violations.map(describeViolation)].join("\n"));
12
+ this.violations = violations;
13
+ }
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { BootContext } from "./boot-context.ts";
2
+ export declare function emitBootWarnings(ctx: BootContext): void;
@@ -0,0 +1,20 @@
1
+ import { warnOnUniqueAccessRoles } from "./access-roles.js";
2
+ import { warnOnToggleableDependencies } from "./config-deps.js";
3
+ import { warnOnNavAccessInversion, warnOnUnreachableNavScreens } from "./nav.js";
4
+ import { warnOnMissingSecurityBaseline } from "./security-baseline.js";
5
+ import { resolveNavAllowlist } from "./workspaces.js";
6
+ // Warnings only make sense for a valid composition, so they run after every
7
+ // validation phase passed.
8
+ export function emitBootWarnings(ctx) {
9
+ const { features, options, allNavQns, allWorkspaceQns } = ctx;
10
+ warnOnNavAccessInversion(allNavQns);
11
+ const effectiveNavAllowlist = resolveNavAllowlist(options?.navAllowlist, allNavQns, allWorkspaceQns);
12
+ if (effectiveNavAllowlist !== undefined) {
13
+ warnOnUnreachableNavScreens(allNavQns, effectiveNavAllowlist, options?.navAllowlistExempt, options?.navAllowlist === undefined ? "workspaces" : "explicit");
14
+ }
15
+ warnOnToggleableDependencies(features, ctx.featureMap);
16
+ warnOnMissingSecurityBaseline(features);
17
+ if (options?.warnOnUniqueAccessRoles === true) {
18
+ warnOnUniqueAccessRoles(features);
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ import { type EntityDefinition } from "@cosmicdrift/kumiko-types/fields";
2
+ import type { FeatureDefinition } from "../types/index.ts";
3
+ export declare function isSortableSqlDerivedField(entity: EntityDefinition, name: string): boolean;
4
+ export declare function validateSqlDerivedFields(features: readonly FeatureDefinition[]): void;
@@ -0,0 +1,184 @@
1
+ import { isSqlDerivedField, } from "@cosmicdrift/kumiko-types/fields";
2
+ import { holdsCiphertextOrSecret } from "../../db/entity-field-encryption.js";
3
+ import { parseRefTargetEntityName } from "../parse-ref-target.js";
4
+ export function isSortableSqlDerivedField(entity, name) {
5
+ const def = entity.derivedFields?.[name];
6
+ return def !== undefined && isSqlDerivedField(def) && def.sortable === true;
7
+ }
8
+ const VALUE_TYPE_OF_FIELD_TYPE = {
9
+ text: "text",
10
+ longText: "text",
11
+ select: "text",
12
+ tz: "text",
13
+ reference: "text",
14
+ number: "number",
15
+ bigInt: "number",
16
+ decimal: "decimal",
17
+ boolean: "boolean",
18
+ date: "date",
19
+ timestamp: "timestamp",
20
+ };
21
+ function valueTypeOfField(entity, field) {
22
+ if (field === "id")
23
+ return "text";
24
+ const def = entity.fields[field];
25
+ return def === undefined ? undefined : VALUE_TYPE_OF_FIELD_TYPE[def.type];
26
+ }
27
+ function requireUsableField(site, entity, field, role) {
28
+ if (field !== "id" && entity.fields[field] === undefined) {
29
+ throw new Error(`${site.where}: ${role} "${field}" is not a field of the entity.`);
30
+ }
31
+ if (valueTypeOfField(entity, field) === undefined) {
32
+ throw new Error(`${site.where}: ${role} "${field}" has type "${entity.fields[field]?.type}", which a SQL derived field cannot read (compound types are not supported; money only as the measure of a related first).`);
33
+ }
34
+ if (holdsCiphertextOrSecret(entity, field)) {
35
+ throw new Error(`${site.where}: ${role} "${field}" is encrypted or secret; a SQL derived field would expose or order by it.`);
36
+ }
37
+ }
38
+ function requireValueType(site, def, expected) {
39
+ if (def.valueType !== expected) {
40
+ throw new Error(`${site.where}: valueType must be "${expected}", got "${def.valueType}".`);
41
+ }
42
+ }
43
+ function requireMoneyMeasure(site, def, related, measure) {
44
+ if (measure.label === true) {
45
+ throw new Error(`${site.where}: label: true needs a reference field, not money.`);
46
+ }
47
+ if (holdsCiphertextOrSecret(related, measure.field)) {
48
+ throw new Error(`${site.where}: measure field "${measure.field}" is encrypted or secret; a SQL derived field would expose or order by it.`);
49
+ }
50
+ requireUsableField(site, related, measure.orderBy, "measure orderBy");
51
+ requireValueType(site, def, "money");
52
+ }
53
+ function validateCondition(site, related, condition) {
54
+ if ("activeOn" in condition) {
55
+ for (const field of [condition.activeOn.from, condition.activeOn.to]) {
56
+ requireUsableField(site, related, field, "activeOn field");
57
+ if (valueTypeOfField(related, field) !== "date") {
58
+ throw new Error(`${site.where}: activeOn field "${field}" must be a date field.`);
59
+ }
60
+ }
61
+ // skip: activeOn carries no op/value to check
62
+ return;
63
+ }
64
+ requireUsableField(site, related, condition.field, "where field");
65
+ const { value, op } = condition;
66
+ const isList = Array.isArray(value);
67
+ if ((op === "in") !== isList) {
68
+ throw new Error(`${site.where}: where "${condition.field}": op "in" takes a list, and only "in" does.`);
69
+ }
70
+ if (value === null && op !== "eq" && op !== "ne") {
71
+ throw new Error(`${site.where}: where "${condition.field}": null is only valid with eq/ne.`);
72
+ }
73
+ if (typeof value === "object" && value !== null && !isList) {
74
+ const expected = "today" in value ? "date" : "timestamp";
75
+ if (valueTypeOfField(related, condition.field) !== expected) {
76
+ throw new Error(`${site.where}: where "${condition.field}": ${expected === "date" ? "today" : "now"} needs a ${expected} field.`);
77
+ }
78
+ }
79
+ }
80
+ // A sortable derived value runs its subquery for every row of the page, so the join column of
81
+ // the related table needs an index; buildEntityTable creates none for a plain reference field.
82
+ function requireJoinIndex(site, related, field, relations) {
83
+ const indexed = related.fields[field]?.type === "file" ||
84
+ related.fields[field]?.type === "image" ||
85
+ Object.values(relations ?? {}).some((relation) => relation.type === "belongsTo" && relation.foreignKey === field) ||
86
+ (related.indexes ?? []).some(({ columns }) => {
87
+ const [first, second] = columns;
88
+ return first === field || (first === "tenantId" && second === field);
89
+ });
90
+ if (!indexed) {
91
+ throw new Error(`${site.where}: sortable needs an index on the related table's "${field}"; declare indexes: [{ columns: ["${field}"] }] (or ["tenantId", "${field}"]) on that entity.`);
92
+ }
93
+ }
94
+ function validateRelated(site, own, def, spec, allEntities, relations) {
95
+ const relatedName = parseRefTargetEntityName(spec.entity);
96
+ const related = allEntities.get(relatedName);
97
+ if (related === undefined) {
98
+ throw new Error(`${site.where}: related entity "${spec.entity}" is not registered.`);
99
+ }
100
+ requireUsableField(site, related, spec.on.field, "on.field");
101
+ if (def.sortable === true) {
102
+ requireJoinIndex(site, related, spec.on.field, relations.get(relatedName));
103
+ }
104
+ requireUsableField(site, own, spec.on.outer ?? "id", "on.outer");
105
+ for (const condition of spec.where ?? [])
106
+ validateCondition(site, related, condition);
107
+ requireMeasureValueType(site, def, spec.measure, related);
108
+ }
109
+ function requireMeasureValueType(site, def, measure, related) {
110
+ if (measure.kind === "count") {
111
+ requireValueType(site, def, "number");
112
+ // skip: count measures no field
113
+ return;
114
+ }
115
+ if (measure.kind === "first" && related.fields[measure.field]?.type === "money") {
116
+ requireMoneyMeasure(site, def, related, measure);
117
+ // skip: money is checked in full above
118
+ return;
119
+ }
120
+ requireUsableField(site, related, measure.field, "measure field");
121
+ if (measure.kind === "countDistinct") {
122
+ requireValueType(site, def, "number");
123
+ // skip: countDistinct has no orderBy or label
124
+ return;
125
+ }
126
+ requireUsableField(site, related, measure.orderBy, "measure orderBy");
127
+ if (measure.label === true) {
128
+ const measured = related.fields[measure.field];
129
+ if (measured?.type !== "reference" || measured.labelField === undefined) {
130
+ throw new Error(`${site.where}: label: true needs a reference field with a labelField.`);
131
+ }
132
+ requireValueType(site, def, "text");
133
+ // skip: a label is always text, the measured field's own type does not apply
134
+ return;
135
+ }
136
+ const expected = valueTypeOfField(related, measure.field);
137
+ if (expected !== undefined)
138
+ requireValueType(site, def, expected);
139
+ }
140
+ function validateCoalesce(site, own, def, fields) {
141
+ if (fields.length === 0)
142
+ throw new Error(`${site.where}: coalesce needs at least one field.`);
143
+ const types = new Set();
144
+ for (const field of fields) {
145
+ requireUsableField(site, own, field, "coalesce field");
146
+ types.add(valueTypeOfField(own, field));
147
+ }
148
+ if (types.size !== 1) {
149
+ throw new Error(`${site.where}: coalesce fields must all have the same type.`);
150
+ }
151
+ const [only] = types;
152
+ if (only !== undefined)
153
+ requireValueType(site, def, only);
154
+ }
155
+ export function validateSqlDerivedFields(features) {
156
+ const allEntities = new Map();
157
+ const relations = new Map();
158
+ for (const feature of features) {
159
+ for (const [name, entity] of Object.entries(feature.entities ?? {})) {
160
+ allEntities.set(name, entity);
161
+ const entityRelations = feature.relations[name];
162
+ if (entityRelations !== undefined)
163
+ relations.set(name, entityRelations);
164
+ }
165
+ }
166
+ for (const [entityName, entity] of allEntities) {
167
+ for (const [key, def] of Object.entries(entity.derivedFields ?? {})) {
168
+ const site = { where: `Entity "${entityName}" derived field "${key}"` };
169
+ if (!isSqlDerivedField(def)) {
170
+ if (def.valueType === "money") {
171
+ throw new Error(`${site.where}: valueType "money" needs a sql derived field (related first on a money field); a JS derive has no currency column.`);
172
+ }
173
+ if ("sortable" in def) {
174
+ throw new Error(`${site.where}: sortable needs a sql derived field; a JS derive cannot order in SQL.`);
175
+ }
176
+ continue;
177
+ }
178
+ if (def.sql.kind === "coalesce")
179
+ validateCoalesce(site, entity, def, def.sql.fields);
180
+ else
181
+ validateRelated(site, entity, def, def.sql, allEntities, relations);
182
+ }
183
+ }
184
+ }
@@ -1,4 +1,5 @@
1
1
  import type { QueryHandlerDef } from "@cosmicdrift/kumiko-types/handlers";
2
+ import type { FieldCondition } from "@cosmicdrift/kumiko-types/screen";
2
3
  import type { FeatureDefinition } from "../types/index.ts";
3
4
  export declare const FILE_FIELD_TYPES: Set<string>;
4
5
  export declare const PII_DIRECT_NAME_HINTS: ReadonlySet<string>;
@@ -23,9 +24,11 @@ export declare function validateReferenceFields(feature: FeatureDefinition, feat
23
24
  export declare function validateEmbeddedFields(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
24
25
  export declare function validateEntitySelectOptionsQuery(feature: FeatureDefinition): void;
25
26
  export declare function validateEntitySelectAvailabilityQuery(feature: FeatureDefinition, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
27
+ export declare function conditionComparisonValues(when: Exclude<FieldCondition, boolean>): readonly unknown[];
26
28
  export declare function validateEntitySelectConditionalOptions(feature: FeatureDefinition): void;
27
29
  export declare function validateMultiSelectFields(feature: FeatureDefinition): void;
28
30
  export declare function validateImageVariants(feature: FeatureDefinition): void;
31
+ export declare const LOCKABLE_FIELD_TYPES: ReadonlySet<string>;
29
32
  export declare function validateLockedWhen(feature: FeatureDefinition): void;
30
33
  export declare function validateTransitions(feature: FeatureDefinition): void;
31
34
  export declare function validateExtendSchemaCollisions(feature: FeatureDefinition): void;