@cosmicdrift/kumiko-framework 0.385.0 → 0.387.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/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/widgets.md +39 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +2 -0
- package/dist/api/request-context.js +16 -7
- package/dist/api/routes.js +2 -2
- package/dist/api/server.js +34 -33
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +15 -15
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.js +4 -4
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cross-tenant-reads.d.ts +8 -0
- package/dist/db/cross-tenant-reads.js +23 -0
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +2 -2
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +4 -4
- package/dist/db/event-store-executor-context.d.ts +1 -1
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read.js +27 -28
- package/dist/db/event-store-executor-write.js +65 -70
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +26 -19
- package/dist/db/migrate-runner.js +25 -25
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/queries/test-stack.d.ts +4 -1
- package/dist/db/queries/test-stack.js +15 -2
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +6 -6
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +82 -82
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +9 -2
- package/dist/derivatives/delete-stored-file.js +5 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +3 -3
- package/dist/engine/boot-validator/entity-handler.js +60 -61
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +1 -1
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +20 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +50 -57
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.js +15 -16
- package/dist/engine/boot-validator/screens-forms.js +10 -11
- package/dist/engine/boot-validator/screens-lists.js +14 -15
- package/dist/engine/boot-validator/screens.js +34 -40
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +75 -75
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +12 -11
- package/dist/engine/define-handler.js +12 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.js +13 -14
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +1 -1
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
- package/dist/engine/feature-ast/patterns.d.ts +2 -2
- package/dist/engine/feature-ast/render.js +2 -2
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +9 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.js +1 -1
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +12 -12
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +40 -38
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +7 -27
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.js +3 -3
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +28 -28
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +6 -7
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +2 -2
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +5 -6
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +11 -5
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +8 -8
- package/dist/internal/stack-support.d.ts +1 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-helpers.d.ts +1 -2
- package/dist/jobs/job-runner-helpers.js +8 -18
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner.js +17 -18
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +10 -10
- package/dist/pipeline/dispatch-shared.d.ts +1 -0
- package/dist/pipeline/dispatch-shared.js +42 -19
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +3 -3
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-delivery.js +1 -1
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/projection-rebuild.js +13 -12
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli.js +116 -22
- package/dist/search/index.js +4 -4
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +10 -9
- package/dist/utils/compare.js +1 -1
- package/package.json +4 -4
- package/src/changes.json +54 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/pii-personal-migration.ts +17 -8
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -6,16 +6,14 @@ import { access as accessPresets } from "../config-helpers.js";
|
|
|
6
6
|
import { parseRefTarget } from "../parse-ref-target.js";
|
|
7
7
|
import { isSelectOptionTone } from "./select-option-tone.js";
|
|
8
8
|
export const FILE_FIELD_TYPES = new Set(["file", "image", "files", "images"]);
|
|
9
|
-
// Field
|
|
10
|
-
// `userOwned` / `tenantOwned` / `allowPlaintext
|
|
11
|
-
//
|
|
9
|
+
// Field names that typically contain PII. Without a `pii: true` /
|
|
10
|
+
// `userOwned` / `tenantOwned` / `allowPlaintext` marker → boot warning.
|
|
11
|
+
// Compare in lower case for case-insensitive matching (displayName vs displayname).
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
// - `name`
|
|
15
|
-
// tenant.name, role.name)
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// Quelle: docs/plans/datenschutz/crypto-shredding.md Boot-Validation-Sektion.
|
|
13
|
+
// Deliberately NOT included:
|
|
14
|
+
// - `name` alone — too many business contexts (product.name,
|
|
15
|
+
// tenant.name, role.name) are not PII. Personal names are detected
|
|
16
|
+
// through displayName / firstName / lastName / fullName.
|
|
19
17
|
export const PII_DIRECT_NAME_HINTS = new Set([
|
|
20
18
|
"email",
|
|
21
19
|
"phone",
|
|
@@ -42,8 +40,8 @@ export const PII_DIRECT_NAME_HINTS = new Set([
|
|
|
42
40
|
"iban",
|
|
43
41
|
"bic",
|
|
44
42
|
]);
|
|
45
|
-
// Field
|
|
46
|
-
// User-Forget
|
|
43
|
+
// Field names that typically contain user-generated content —
|
|
44
|
+
// User-Forget must encrypt these with the author subject key.
|
|
47
45
|
export const PII_USER_OWNED_NAME_HINTS = new Set([
|
|
48
46
|
"body",
|
|
49
47
|
"text",
|
|
@@ -305,18 +303,18 @@ export function validateAnonymousRateLimit(featureName, kind, handlerName, acces
|
|
|
305
303
|
`Use rateLimit.per="ip" or "ip+handler" for anonymous endpoints.`);
|
|
306
304
|
}
|
|
307
305
|
}
|
|
308
|
-
// --- MultiStreamProjection delivery
|
|
306
|
+
// --- MultiStreamProjection delivery invariant ---
|
|
309
307
|
//
|
|
310
|
-
// `delivery: "per-instance"`
|
|
311
|
-
// N
|
|
312
|
-
//
|
|
313
|
-
// kumiko_event_consumers
|
|
314
|
-
//
|
|
308
|
+
// `delivery: "per-instance"` with a `table` is a semantic trap:
|
|
309
|
+
// N dispatcher instances would write the same INSERT/UPDATE rows in parallel
|
|
310
|
+
// (races / duplicates), and a rebuild would affect only one row in
|
|
311
|
+
// kumiko_event_consumers (the SHARED_INSTANCE_SENTINEL row),
|
|
312
|
+
// while live cursors reside in per-instance rows → cursor divergence.
|
|
315
313
|
//
|
|
316
|
-
//
|
|
317
|
-
// in-memory cache invalidation).
|
|
318
|
-
// shared delivery —
|
|
319
|
-
//
|
|
314
|
+
// The invariant is: per-instance consumers are side-effect-only (SSE,
|
|
315
|
+
// in-memory cache invalidation). Materializing a table requires
|
|
316
|
+
// shared delivery — it is exactly-once globally and gives the rebuild
|
|
317
|
+
// a single cursor to reset.
|
|
320
318
|
export function validateMultiStreamProjections(feature) {
|
|
321
319
|
for (const [name, msp] of Object.entries(feature.multiStreamProjections)) {
|
|
322
320
|
if (msp.delivery === "per-instance" && msp.table !== undefined) {
|
|
@@ -326,18 +324,19 @@ export function validateMultiStreamProjections(feature) {
|
|
|
326
324
|
}
|
|
327
325
|
}
|
|
328
326
|
}
|
|
329
|
-
// --- Located-
|
|
327
|
+
// --- Located-timestamp validation ---
|
|
330
328
|
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
329
|
+
// When a field with `type: "timestamp"` has a `locatedBy` marker, the
|
|
330
|
+
// referenced field must exist in the same entity AND have type `tz`.
|
|
331
|
+
// Otherwise neither the DB wrapper nor the JSON serializer knows which TZ
|
|
332
|
+
// belongs to the wall clock → silent data loss when reading in another
|
|
333
|
+
// server TZ.
|
|
335
334
|
//
|
|
336
|
-
//
|
|
335
|
+
// Manual construction is the most common source of conflicts:
|
|
337
336
|
// { foo: { type: "timestamp", locatedBy: "fooTz" } }
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
337
|
+
// without declaring the `fooTz` field. `createLocatedTimestampField()`
|
|
338
|
+
// instead creates ONE field of type "locatedTimestamp" — using it avoids
|
|
339
|
+
// this validator.
|
|
341
340
|
export function validateLocatedTimestamps(feature) {
|
|
342
341
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
343
342
|
const fields = entity.fields;
|
|
@@ -359,18 +358,18 @@ export function validateLocatedTimestamps(feature) {
|
|
|
359
358
|
}
|
|
360
359
|
}
|
|
361
360
|
}
|
|
362
|
-
// --- Entity-
|
|
361
|
+
// --- Entity-index validation ---
|
|
363
362
|
//
|
|
364
|
-
// entity.indexes
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// in
|
|
368
|
-
// generate
|
|
363
|
+
// entity.indexes declares composite/unique indexes over multiple field
|
|
364
|
+
// columns. Common errors include misspelled field names, an empty column
|
|
365
|
+
// list, and an index on a field for which the DB has no column (file/image
|
|
366
|
+
// in the multi-value variant). Catch these at boot, long before drizzle-kit
|
|
367
|
+
// fails during the generate run.
|
|
369
368
|
//
|
|
370
|
-
// `tenantId
|
|
371
|
-
//
|
|
372
|
-
// (`["tenantId", "key"]`
|
|
373
|
-
//
|
|
369
|
+
// A `tenantId`-only index is redundant because buildEntityTable creates
|
|
370
|
+
// that index automatically. Composite forms remain allowed
|
|
371
|
+
// (`["tenantId", "key"]` is useful); only the tenantId-only single-column
|
|
372
|
+
// form is rejected.
|
|
374
373
|
export function validateEntityIndexes(feature) {
|
|
375
374
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
376
375
|
if (!entity.indexes)
|
|
@@ -397,21 +396,21 @@ export function validateEntityIndexes(feature) {
|
|
|
397
396
|
`a jsonb array cannot be indexed as a column. Use a single-value field or remove from the index.`);
|
|
398
397
|
}
|
|
399
398
|
if (field && field.type === "longText") {
|
|
400
|
-
// longText
|
|
401
|
-
// BTREE
|
|
402
|
-
// (PG
|
|
403
|
-
//
|
|
404
|
-
// longText
|
|
405
|
-
//
|
|
406
|
-
//
|
|
399
|
+
// longText semantically means "potentially-megabytes content" — a
|
|
400
|
+
// BTREE index on a 1-MB column is a performance disaster
|
|
401
|
+
// (PG would need to dereference TOAST pages for every
|
|
402
|
+
// index lookup). This is consistent with the type-level decision
|
|
403
|
+
// that longText has no sortable/searchable/filterable flags. Anyone
|
|
404
|
+
// who truly needs indexing should use `text` with the
|
|
405
|
+
// corresponding scaling trade-offs.
|
|
407
406
|
throw new Error(`${where}: column "${col}" is a longText field — these cannot be indexed. ` +
|
|
408
407
|
`Use \`text\` if you need indexing, or rely on the SearchAdapter (Meilisearch) for full-text search on long content.`);
|
|
409
408
|
}
|
|
410
409
|
}
|
|
411
|
-
// UNIQUE
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
// column
|
|
410
|
+
// A UNIQUE constraint on tenantId is semantically meaningful (1:1 tenant→entity)
|
|
411
|
+
// and NOT redundant — buildEntityTable's automatic index is only a
|
|
412
|
+
// performance hint, not a constraint. Only the tenantId-only,
|
|
413
|
+
// single-column, non-unique form is rejected.
|
|
415
414
|
if (def.columns.length === 1 && def.columns[0] === "tenantId" && !def.unique) {
|
|
416
415
|
throw new Error(`${where}: single-column index on "tenantId" is redundant — ` +
|
|
417
416
|
`buildEntityTable always creates one automatically. Remove this entry.`);
|
|
@@ -436,9 +435,9 @@ export function validateEncryptedFields(feature) {
|
|
|
436
435
|
let found = false;
|
|
437
436
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
438
437
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
439
|
-
//
|
|
440
|
-
// searchable/sortable
|
|
441
|
-
// (longText
|
|
438
|
+
// Both string-typed fields can be encrypted. The
|
|
439
|
+
// searchable/sortable conflict checks apply only to `text`
|
|
440
|
+
// (`longText` does not have these flags at the type level).
|
|
442
441
|
if (field.type !== "text" && field.type !== "longText")
|
|
443
442
|
continue;
|
|
444
443
|
if (!field.encrypted)
|
|
@@ -491,7 +490,7 @@ const NUMERIC_EMBEDDED_SUB_TYPES = new Set(["number", "money", "decimal"]);
|
|
|
491
490
|
function isValidEmbeddedDecimalScale(scale) {
|
|
492
491
|
return Number.isInteger(scale) && scale >= 0 && scale <= 15;
|
|
493
492
|
}
|
|
494
|
-
//
|
|
493
|
+
// ReferenceFieldDef validation, shared by
|
|
495
494
|
// top-level reference fields and reference sub-fields of an embedded field
|
|
496
495
|
// (only the field-path in error messages differs, e.g. "accountId" vs
|
|
497
496
|
// "lines.accountId", so a failure is locatable either way).
|
|
@@ -552,7 +551,7 @@ function validateReferenceTarget(entityName, fieldPath, refString, labelField, o
|
|
|
552
551
|
// (use-reference-lookup).
|
|
553
552
|
validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feature, queryHandlers);
|
|
554
553
|
}
|
|
555
|
-
//
|
|
554
|
+
// The picker may source its options from a query handler instead of
|
|
556
555
|
// the referenced entity's table. A typo'd QN would only surface as a 404 on
|
|
557
556
|
// first Combobox open, so it is pinned at boot — same treatment as
|
|
558
557
|
// `DashboardFilterDefinition.optionsQuery` (boot-validator/query-refs.ts).
|
|
@@ -571,7 +570,7 @@ function validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feat
|
|
|
571
570
|
`is declared via r.queryHandler(...).`);
|
|
572
571
|
}
|
|
573
572
|
}
|
|
574
|
-
//
|
|
573
|
+
// ReferenceFieldDef validation for top-level
|
|
575
574
|
// reference fields (self-reference, entity → entity, is allowed). The actual
|
|
576
575
|
// checks run in validateReferenceTarget — shared with the reference
|
|
577
576
|
// sub-fields of an embedded field (validateEmbeddedFields).
|
|
@@ -705,10 +704,10 @@ function validateEmbeddedListMetadata(fieldName, entityName, field, entity) {
|
|
|
705
704
|
}
|
|
706
705
|
// --- MultiSelect field validation ---
|
|
707
706
|
//
|
|
708
|
-
// options
|
|
709
|
-
// default —
|
|
710
|
-
//
|
|
711
|
-
//
|
|
707
|
+
// options must be non-empty (otherwise the field cannot be used), and
|
|
708
|
+
// default — when set — must be a subset of options. Both conditions would
|
|
709
|
+
// also fail at runtime in the Zod schema; boot validation only provides
|
|
710
|
+
// the earliest clear error messages.
|
|
712
711
|
// Exhaustive over MultiSelectFieldDef["columns"] (the compiler flags a union
|
|
713
712
|
// change); the TS union is gone at runtime for JS/JSON-sourced definitions.
|
|
714
713
|
const CHECKBOX_COLUMN_COUNTS = {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
1
3
|
// Providers whose bytes do not survive a process restart. Only "inmemory"
|
|
2
4
|
// today; extend if another ephemeral bundled provider lands.
|
|
3
5
|
const EPHEMERAL_PROVIDERS = new Set(["inmemory"]);
|
|
@@ -30,8 +32,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
30
32
|
}
|
|
31
33
|
const persistent = [...registeredProviders].filter((p) => !EPHEMERAL_PROVIDERS.has(p));
|
|
32
34
|
if (persistent.length === 0) {
|
|
33
|
-
|
|
34
|
-
console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
|
|
35
|
+
log.warn("user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
|
|
35
36
|
// skip: ephemeral store already warned; the s3-env env-var check below is moot
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
@@ -41,8 +42,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
41
42
|
if (persistent.length === 1 && persistent[0] === "s3-env") {
|
|
42
43
|
const missing = S3_ENV_REQUIRED.filter((v) => !process.env[v]);
|
|
43
44
|
if (missing.length > 0) {
|
|
44
|
-
|
|
45
|
-
console.warn(`[kumiko:boot] createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
|
|
45
|
+
log.warn(`createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -11,7 +11,7 @@ function requiredKeysFromGeneratedConfigHub(schema) {
|
|
|
11
11
|
const out = new Set();
|
|
12
12
|
// The generator's section titles + mask-title field-label overrides are
|
|
13
13
|
// dot-form i18n references (`${feature}.settings`), never literal display
|
|
14
|
-
// text, so treatDotFormAsKey bypasses isI18nKey's colon-only check
|
|
14
|
+
// text, so treatDotFormAsKey bypasses isI18nKey's colon-only check.
|
|
15
15
|
for (const screen of schema.screens) {
|
|
16
16
|
for (const key of requiredKeysFromScreen(SETTINGS_HUB_FEATURE, screen, {
|
|
17
17
|
treatDotFormAsKey: true,
|
|
@@ -9,14 +9,14 @@ export type ValidateBootOptions = {
|
|
|
9
9
|
/** Warn when an access role is used by exactly one handler/config-key/
|
|
10
10
|
* field across the whole boot scan — often a typo, but also the normal
|
|
11
11
|
* shape of a legitimate fine-grained role (one role scoped to one admin
|
|
12
|
-
* endpoint on purpose). Opt-in (default false
|
|
12
|
+
* endpoint on purpose). Opt-in (default false): a default-on
|
|
13
13
|
* prod warning that nobody can silence per-role isn't worth the noise
|
|
14
14
|
* it generates on every boot. */
|
|
15
15
|
readonly warnOnUniqueAccessRoles?: boolean;
|
|
16
16
|
/** QNs the app's sidebar allowlist admits. When set, every declared nav
|
|
17
17
|
* whose screen isn't reachable from any allowlisted nav entry gets a
|
|
18
18
|
* boot-time warning — that screen would otherwise be silently
|
|
19
|
-
* unreachable via the sidebar
|
|
19
|
+
* unreachable via the sidebar. When omitted and the app has
|
|
20
20
|
* workspaces, the allowlist is derived from `r.workspace({ nav })` and
|
|
21
21
|
* `r.nav({ workspaces })` assignments instead. */
|
|
22
22
|
readonly navAllowlist?: ReadonlySet<string>;
|
|
@@ -30,9 +30,9 @@ import { validateExtensionSelectorPanels } from "./selector-owner-panels.js";
|
|
|
30
30
|
import { validateTransferGraph } from "./transfer-graph.js";
|
|
31
31
|
import { validateSubmitPrefilledHiddenFields } from "./url-prefill-fields.js";
|
|
32
32
|
import { collectWorkspaceQns, resolveNavAllowlist, validateDefaultWorkspaceUniqueness, validateWorkspaces, } from "./workspaces.js";
|
|
33
|
-
// Re-export:
|
|
34
|
-
//
|
|
35
|
-
//
|
|
33
|
+
// Re-export: run-dev-app.ts needs this to pass write-handler QNs
|
|
34
|
+
// to codegen. It is not part of validateBoot, but uses the same
|
|
35
|
+
// extraction logic.
|
|
36
36
|
export { collectCardMetaOverflow } from "./card-meta-overflow.js";
|
|
37
37
|
export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
|
|
38
38
|
export { collectWriteHandlerQns } from "./nav.js";
|
|
@@ -101,13 +101,13 @@ export function validateBoot(rawFeatures, options) {
|
|
|
101
101
|
}
|
|
102
102
|
// Collect all config keys across features (for cross-feature reference validation)
|
|
103
103
|
const allConfigKeys = new Set();
|
|
104
|
-
// Qualified config-key set
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// `…:config:branding-title`;
|
|
109
|
-
// multi-word
|
|
110
|
-
// `feature.short
|
|
104
|
+
// Qualified config-key set for ConfigEditScreen validation. MUST be
|
|
105
|
+
// canonicalized via qualifyEntityName (toKebab on feature + key), exactly
|
|
106
|
+
// as define-feature/registry builds the QN. The raw f.configKeys key is
|
|
107
|
+
// the camelCase object key (`brandingTitle`), but the actual QN is
|
|
108
|
+
// `…:config:branding-title`; without toKebab, every configEdit screen
|
|
109
|
+
// with a multi-word config key would fail incorrectly. allConfigKeys above
|
|
110
|
+
// uses the older `feature.short` format for validateConfigReads.
|
|
111
111
|
const configKeyDefsByQn = new Map();
|
|
112
112
|
for (const f of features) {
|
|
113
113
|
for (const [key, def] of Object.entries(f.configKeys)) {
|
|
@@ -138,15 +138,15 @@ export function validateBoot(rawFeatures, options) {
|
|
|
138
138
|
const screensByShortId = collectScreensByShortId(features);
|
|
139
139
|
validateScreenShortIdCollisions(screensByShortId);
|
|
140
140
|
// Cross-feature entity → detailFor-screen map — built (+ uniqueness-
|
|
141
|
-
// validated) up front so per-feature rowAction entity-targets
|
|
141
|
+
// validated) up front so per-feature rowAction entity-targets
|
|
142
142
|
// resolve against it inside the loop below, instead of at the very end
|
|
143
143
|
// where this used to run.
|
|
144
144
|
const detailForScreens = collectDetailForScreens(features, featureMap);
|
|
145
|
-
// Cross-feature API exposure
|
|
146
|
-
// r.exposesApi(name). Per-feature validateApiExposureMatching
|
|
147
|
-
// usedApis
|
|
148
|
-
//
|
|
149
|
-
//
|
|
145
|
+
// Cross-feature API exposure map — each feature declares markers via
|
|
146
|
+
// r.exposesApi(name). Per-feature validateApiExposureMatching walks the
|
|
147
|
+
// usedApis set and checks that every entry has a match here.
|
|
148
|
+
// This prevents typoed or removed QN calls from causing a runtime crash
|
|
149
|
+
// instead of a boot failure.
|
|
150
150
|
const allExposedApis = new Map(); // apiName → providerFeature
|
|
151
151
|
for (const f of features) {
|
|
152
152
|
for (const apiName of f.exposedApis) {
|
|
@@ -200,10 +200,10 @@ export function validateBoot(rawFeatures, options) {
|
|
|
200
200
|
validateParentRefs(feature, featureMap);
|
|
201
201
|
validateTransferGraph(feature, featureMap);
|
|
202
202
|
validateMultiStreamProjections(feature);
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// Function
|
|
203
|
+
// Before validateScreens: its visible/entityId field-reference checks throw
|
|
204
|
+
// for a Function value first, with the confusing "unknown field undefined"
|
|
205
|
+
// message (because `typeof fn !== "boolean"` is true), before the clear
|
|
206
|
+
// Function check here can run.
|
|
207
207
|
validateActionWiring(feature);
|
|
208
208
|
validateFieldWiring(feature);
|
|
209
209
|
validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Nav validation + collectKnownRoles (role synthesis for ownership
|
|
2
2
|
// validation in index.ts — thematically adjacent to ownership.ts, kept
|
|
3
3
|
// here since this is a structural move, not a semantic reorg).
|
|
4
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
4
5
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
5
6
|
import { isOpenToAllGranted } from "../types/index.js";
|
|
7
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
6
8
|
export function collectWriteHandlerQns(features) {
|
|
7
9
|
const set = new Set();
|
|
8
10
|
for (const f of features) {
|
|
@@ -110,7 +112,7 @@ function navViewerRoles(access) {
|
|
|
110
112
|
return isOpenToAllGranted(access) ? undefined : [];
|
|
111
113
|
return access.roles;
|
|
112
114
|
}
|
|
113
|
-
//
|
|
115
|
+
// Warn (never throw — this is a real UX bug, not a broken ref)
|
|
114
116
|
// when a nav leaf's role gate is disjoint from its parent section's — no
|
|
115
117
|
// user who can see the section could ever see this child, so the section
|
|
116
118
|
// renders with zero visible children for every user who reaches it.
|
|
@@ -133,8 +135,7 @@ export function warnOnNavAccessInversion(allNavQns) {
|
|
|
133
135
|
continue; // parent already visible to nobody — a different problem
|
|
134
136
|
if (leafRoles.some((role) => parentRoles.includes(role)))
|
|
135
137
|
continue;
|
|
136
|
-
|
|
137
|
-
console.warn(`[kumiko:boot] Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
|
|
138
|
+
log.warn(`Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
|
|
138
139
|
`"${navDef.parent}" only allows [${parentRoles.join(", ")}] — no user who can see the ` +
|
|
139
140
|
`parent section could ever see this entry. If this is intentional, ignore this warning; ` +
|
|
140
141
|
`otherwise align the role sets.`);
|
|
@@ -158,8 +159,7 @@ export function warnOnUnreachableNavScreens(allNavQns, allowedNavQns, navAllowli
|
|
|
158
159
|
const remedy = allowlistSource === "workspaces"
|
|
159
160
|
? "add it to a workspace's `nav` array or set `workspaces` on the nav"
|
|
160
161
|
: "add it to the allowlist";
|
|
161
|
-
|
|
162
|
-
console.warn(`[kumiko:boot] Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
|
|
162
|
+
log.warn(`Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
|
|
163
163
|
`screen "${screenQn}", which no allowlisted nav entry reaches — it is unreachable via ` +
|
|
164
164
|
`the sidebar. If this is intentional (e.g. reachable through a generated hub), add ` +
|
|
165
165
|
`"${qn}" to navAllowlistExempt; otherwise ${remedy}.`);
|
|
@@ -3,23 +3,23 @@ import { tableNameOf, } from "../ownership.js";
|
|
|
3
3
|
import { SYSTEM_USER_ID } from "../system-user.js";
|
|
4
4
|
import { SYSTEM_TENANT_ID } from "../types/identifiers.js";
|
|
5
5
|
import { assertQualifiedWhereFragment, tableColumnSqlNames } from "../where-rule-lint.js";
|
|
6
|
-
// --- Ownership rule validation
|
|
6
|
+
// --- Ownership rule validation ---
|
|
7
7
|
//
|
|
8
8
|
// Walks every entity.access and every field.access map, resolves each
|
|
9
9
|
// FromRule against the cross-feature claim registry, and confirms the
|
|
10
10
|
// referenced column exists on the entity. Catches typos, renames, and
|
|
11
11
|
// cross-feature-claim-removal scenarios at boot instead of at request time.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
13
|
+
// Also probes every `{ kind: "where" }` rule against the where-rule
|
|
14
14
|
// lint (see ../where-rule-lint.ts) so a fail-open unqualified-column bug
|
|
15
15
|
// fails the boot instead of waiting for a request from the right role.
|
|
16
16
|
//
|
|
17
17
|
// Field-level `access.read` rejects where-rules too: filterFieldsForRead
|
|
18
18
|
// evaluates it in memory and userCanReadFieldRow fails closed on a where-rule.
|
|
19
19
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// `access.write` maps reject `{ kind: "where" }` outright. A where-rule is
|
|
21
|
+
// raw SQL and the write path never reaches SQL — it evaluates rules in memory
|
|
22
|
+
// against the concrete row (userCanCreateFieldRow /
|
|
23
23
|
// userCanWriteFieldRow), and a create has no row to run a predicate against
|
|
24
24
|
// at all. Such a rule can therefore only ever deny, so the boot fails instead
|
|
25
25
|
// of shipping an access map that silently locks the role out.
|
|
@@ -49,9 +49,8 @@ export function collectClaimKeys(features) {
|
|
|
49
49
|
// corpora go in as undefined — a three-feature stack genuinely does not know
|
|
50
50
|
// which roles and claims the whole app declares, and answering that question
|
|
51
51
|
// from a slice produces false "unknown role"/"unknown claim" errors. The
|
|
52
|
-
// subset-invariant checks (
|
|
53
|
-
//
|
|
54
|
-
// mounted.
|
|
52
|
+
// subset-invariant checks (where-lint, where-on-write, unknown column) run in
|
|
53
|
+
// full: those hold or fail per feature, whatever else is mounted.
|
|
55
54
|
export function validateOwnershipBoot(features) {
|
|
56
55
|
for (const feature of features) {
|
|
57
56
|
validateOwnershipRules(feature, undefined, undefined);
|
|
@@ -258,12 +257,16 @@ export function probeWhereRule(rule, roleName, probe, scope, featureName) {
|
|
|
258
257
|
return;
|
|
259
258
|
assertQualifiedWhereFragment(fragment.sqlText, probe.columns, `${scope} (role "${roleName}", feature: "${featureName}")`);
|
|
260
259
|
}
|
|
260
|
+
// Array.isArray does not narrow a readonly array out of a union.
|
|
261
|
+
function isRoleList(access) {
|
|
262
|
+
return Array.isArray(access);
|
|
263
|
+
}
|
|
261
264
|
// Embedded sub-fields are filtered in memory too (filterFieldsForRead), so a
|
|
262
265
|
// where-rule there is just as unevaluable as on a top-level field.
|
|
263
266
|
function rejectWhereInEmbeddedRead(schema, parentScope, featureName) {
|
|
264
267
|
for (const [subName, sub] of Object.entries(schema)) {
|
|
265
268
|
const map = sub.access?.read;
|
|
266
|
-
if (!map ||
|
|
269
|
+
if (!map || isRoleList(map))
|
|
267
270
|
continue;
|
|
268
271
|
for (const [roleName, rule] of Object.entries(map)) {
|
|
269
272
|
if (rule !== "all" && rule.kind === "where") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
-
// --- Parent-ref boot validation
|
|
2
|
+
// --- Parent-ref boot validation ---
|
|
3
3
|
//
|
|
4
4
|
// Join-row entities (EntityDefinition.parentRef) derive both the write-path
|
|
5
5
|
// deny and the read-path SQL gate from one declaration. This is the boot-time
|