@cosmicdrift/kumiko-framework 0.386.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/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/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/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.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/jobs/job-runner-helpers.js +8 -2
- 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 +13 -15
- 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 +15 -17
- 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 +48 -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
package/dist/random/generate.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
// Random
|
|
2
|
-
// - Tenant
|
|
3
|
-
// - Webhook
|
|
4
|
-
// - API
|
|
5
|
-
// - Tenant
|
|
6
|
-
// - Test
|
|
1
|
+
// Random generator for human-readable resource identifiers:
|
|
2
|
+
// - Tenant keys ("happy-cloud", "swift-river-k8x")
|
|
3
|
+
// - Webhook subscriber slugs ("bold-falcon-receiver" with custom nouns)
|
|
4
|
+
// - API key display names (offer 3 suggestions on creation)
|
|
5
|
+
// - Tenant subdomain suggestions in the subdomain setup screen
|
|
6
|
+
// - Test fixtures with readable identifiers
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// Authority
|
|
11
|
-
//
|
|
8
|
+
// NOT for security tokens (CSRF, session, API keys, OAuth state,
|
|
9
|
+
// reset tokens). Math.random() is not cryptographically unpredictable.
|
|
10
|
+
// Authority binding for tenant keys uses JWT + DB lookup, not slug secrecy;
|
|
11
|
+
// the slug may be guessable.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// Safe across environments: Math.random() runs in Bun, Node, Metro/RN, and
|
|
14
|
+
// Expo Web. No node:crypto imports.
|
|
15
15
|
import { ADJECTIVES, NOUNS } from "./words.js";
|
|
16
|
-
// Alphabet
|
|
17
|
-
// -
|
|
18
|
-
// -
|
|
19
|
-
//
|
|
20
|
-
//
|
|
16
|
+
// Alphabet without characters that are easily confused when entered manually:
|
|
17
|
+
// - no 0/O (zero versus uppercase O)
|
|
18
|
+
// - no 1/l/I (one versus lowercase L versus uppercase I)
|
|
19
|
+
// Result: 32 characters, safe for spelling over the phone AND for copying
|
|
20
|
+
// from email text in unfamiliar fonts.
|
|
21
21
|
const NO_CONFUSABLE_ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
22
22
|
const NO_CONFUSABLE_CHARS = Object.freeze(NO_CONFUSABLE_ALPHABET.split(""));
|
|
23
23
|
function pickRandom(arr) {
|
|
@@ -25,15 +25,15 @@ function pickRandom(arr) {
|
|
|
25
25
|
throw new Error("pickRandom: cannot pick from empty array");
|
|
26
26
|
}
|
|
27
27
|
const value = arr[Math.floor(Math.random() * arr.length)];
|
|
28
|
-
//
|
|
28
|
+
// The length check above guarantees that the index is in range.
|
|
29
29
|
if (value === undefined) {
|
|
30
30
|
throw new Error("pickRandom: indexed value undefined (sparse array?)");
|
|
31
31
|
}
|
|
32
32
|
return value;
|
|
33
33
|
}
|
|
34
|
-
/** Sync
|
|
35
|
-
* (
|
|
36
|
-
*
|
|
34
|
+
/** Sync generator: produces "happy-cloud" or "happy-cloud-k8x"
|
|
35
|
+
* (with suffix). No conflict check — the caller is responsible for
|
|
36
|
+
* uniqueness, or can use generateUniqueName() for the async variant. */
|
|
37
37
|
export function generateAdjNounName(options = {}) {
|
|
38
38
|
const sep = options.separator ?? "-";
|
|
39
39
|
const adjs = options.adjectives ?? ADJECTIVES;
|
|
@@ -44,9 +44,9 @@ export function generateAdjNounName(options = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
return name;
|
|
46
46
|
}
|
|
47
|
-
/** Random
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
/** Random string from the no-confusable alphabet. For suffixes after
|
|
48
|
+
* collisions or as standalone short IDs (e.g. webhook verification codes
|
|
49
|
+
* to be read aloud over the phone). length ≥ 1. */
|
|
50
50
|
export function generateNoConfusableId(length) {
|
|
51
51
|
if (length < 1) {
|
|
52
52
|
throw new Error(`generateNoConfusableId: length must be ≥ 1 (got ${length})`);
|
|
@@ -57,17 +57,17 @@ export function generateNoConfusableId(length) {
|
|
|
57
57
|
}
|
|
58
58
|
return id;
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
* Attempts.
|
|
63
|
-
*
|
|
60
|
+
/** Generates a unique Adj-Noun slug by trying up to maxCleanAttempts
|
|
61
|
+
* clean word pairs, then using a random suffix up to maxTotal-
|
|
62
|
+
* Attempts. Throws if that also fails (= the caller has a bug
|
|
63
|
+
* in isAvailable, or the word list is defective).
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* Example:
|
|
66
66
|
* const slug = await generateUniqueName({
|
|
67
67
|
* isAvailable: async (s) =>
|
|
68
68
|
* !(await db.select().from(tenants).where(eq(tenants.tenantKey, s)).then(r => r.length > 0)),
|
|
69
69
|
* });
|
|
70
|
-
* // → "happy-cloud"
|
|
70
|
+
* // → "happy-cloud" or "happy-cloud-k8x" on collision
|
|
71
71
|
*/
|
|
72
72
|
export async function generateUniqueName(options) {
|
|
73
73
|
const maxClean = options.maxCleanAttempts ?? 3;
|
package/dist/random/words.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// PII-
|
|
1
|
+
// Word lists for human-readable resource slugs (tenant keys, webhook-
|
|
2
|
+
// subscribers, API-key display names, test fixtures). Heroku/Docker-
|
|
3
|
+
// style — adjective + noun produce pronounceable, memorable,
|
|
4
|
+
// PII-free identifiers.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// -
|
|
8
|
-
// -
|
|
9
|
-
// -
|
|
10
|
-
// -
|
|
11
|
-
// - Lowercase, ASCII-only,
|
|
12
|
-
// - 3
|
|
13
|
-
// -
|
|
14
|
-
// -
|
|
6
|
+
// Selection criteria:
|
|
7
|
+
// - Emotionally neutral and professional (no "fluffy", "sneaky")
|
|
8
|
+
// - No animal names (Heroku cliché)
|
|
9
|
+
// - No personal names (cultural appropriation, celebrity collisions)
|
|
10
|
+
// - No thematic clusters (neither weather-only nor geography-only)
|
|
11
|
+
// - Lowercase, ASCII-only, with no hyphens within a word
|
|
12
|
+
// - 3–10 letters per word (compact slug)
|
|
13
|
+
// - Pronounceable in German AND English (user phone support)
|
|
14
|
+
// - No words with ambiguous meanings in English and German
|
|
15
15
|
//
|
|
16
|
-
// 191 × 173 = 33
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// 191 × 173 = 33,043 clean combinations. Birthday bound: from ~180
|
|
17
|
+
// tenants onward, the first collision becomes likely cumulatively (~50%);
|
|
18
|
+
// for an individual draw, p(collision) remains < 1% (see generate.ts). On
|
|
19
|
+
// collision, the suffix path in generateUniqueName is used.
|
|
20
20
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// Extension: appending more words below is sufficient (sorting helps with
|
|
22
|
+
// reviews but is not required). Callers can also provide custom lists via
|
|
23
|
+
// generateUniqueName({ adjectives, nouns }).
|
|
24
24
|
export const ADJECTIVES = Object.freeze([
|
|
25
25
|
"agile",
|
|
26
26
|
"amber",
|
|
@@ -108,21 +108,25 @@ export function createFailOpenReporter(label, meter, now = Date.now) {
|
|
|
108
108
|
lastWarnAt = nowMs;
|
|
109
109
|
logger.warn("rate limiter unavailable, failing open", {
|
|
110
110
|
label,
|
|
111
|
-
// ioredis errors can carry the command args, i.e. the bucket key
|
|
111
|
+
// ioredis errors can carry the command args, i.e. the bucket key. scrubLogText masks an e-mail
|
|
112
|
+
// address in it; the client IP stays in the line as an operational datum.
|
|
112
113
|
error: scrubLogText(err instanceof Error ? err.message : String(err)),
|
|
113
114
|
});
|
|
114
115
|
};
|
|
115
116
|
}
|
|
117
|
+
const failClosedLogger = createFallbackLogger("rate-limit");
|
|
116
118
|
function defaultOnFailClosed(label) {
|
|
117
119
|
return (err) => {
|
|
118
120
|
// Loud by default — fail-closed for an unknown reason is an ops
|
|
119
121
|
// signal, not something to swallow silently. Production deploys
|
|
120
122
|
// override this with a structured logger; the default keeps the
|
|
121
123
|
// noise visible if no logger is wired.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
failClosedLogger.error("fail-closed (refusing request)", {
|
|
125
|
+
label,
|
|
126
|
+
// ioredis errors can carry the command args, i.e. the bucket key. scrubLogText masks an e-mail
|
|
127
|
+
// address in it; the client IP stays in the line as an operational datum.
|
|
128
|
+
error: scrubLogText(err instanceof Error ? err.message : String(err)),
|
|
129
|
+
});
|
|
126
130
|
};
|
|
127
131
|
}
|
|
128
132
|
function setRateLimitHeaders(c, decision) {
|
package/dist/redis/client.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// production defaults stay consistent and env-var wiring lives in one
|
|
9
9
|
// place. Tests / one-off scripts can still `new Redis()` directly.
|
|
10
10
|
//
|
|
11
|
-
// Split out of index.ts
|
|
12
|
-
//
|
|
11
|
+
// Split out of index.ts so pubsub-signal.ts can import this without a
|
|
12
|
+
// same-directory cycle back through the barrel.
|
|
13
13
|
import { Redis } from "ioredis";
|
|
14
14
|
import { scrubLogText } from "../logging/scrub.js";
|
|
15
15
|
import { readPositiveIntEnv } from "../utils/env-parse.js";
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
1
2
|
import { createRedisClient } from "./client.js";
|
|
2
3
|
// Redis emits 'error' on connection blips (reconnects transparently); an
|
|
3
|
-
// unhandled listener crashes the process
|
|
4
|
-
// class with BullMQ's internal client
|
|
5
|
-
function logConnectionError(
|
|
4
|
+
// unhandled listener crashes the process — job-runner hit the same
|
|
5
|
+
// class with BullMQ's internal client.
|
|
6
|
+
function logConnectionError(log, role) {
|
|
6
7
|
return (err) => {
|
|
7
|
-
|
|
8
|
-
console.error(`[kumiko:${label}] ${role} connection error:`, err.message);
|
|
8
|
+
log.error(`${role} connection error`, { error: err.message });
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
export function createRedisPubSubSignal(opts) {
|
|
12
|
+
const log = createFallbackLogger(`kumiko:${opts.label}`);
|
|
12
13
|
const publisher = createRedisClient(opts.redisUrl, opts.clientOptions);
|
|
13
14
|
const subscriber = createRedisClient(opts.redisUrl, opts.clientOptions);
|
|
14
|
-
publisher.on("error", logConnectionError(
|
|
15
|
-
subscriber.on("error", logConnectionError(
|
|
15
|
+
publisher.on("error", logConnectionError(log, "publisher"));
|
|
16
|
+
subscriber.on("error", logConnectionError(log, "subscriber"));
|
|
16
17
|
const listeners = new Set();
|
|
17
18
|
const reconnectListeners = new Set();
|
|
18
19
|
// Sole purpose: stop close() from racing the initial psubscribe (see
|
|
@@ -26,8 +27,7 @@ export function createRedisPubSubSignal(opts) {
|
|
|
26
27
|
payload = JSON.parse(message);
|
|
27
28
|
}
|
|
28
29
|
catch (err) {
|
|
29
|
-
|
|
30
|
-
console.error(`[kumiko:${opts.label}] dropping unparseable message on "${channel}":`, err);
|
|
30
|
+
log.error(`dropping unparseable message on "${channel}"`, { err });
|
|
31
31
|
// skip: unparseable payload already logged above, nothing to deliver
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
@@ -37,14 +37,14 @@ export function createRedisPubSubSignal(opts) {
|
|
|
37
37
|
// ponytail: psubscribe on one pattern rather than per-channel
|
|
38
38
|
// subscribe/unsubscribe — every instance receives every message that
|
|
39
39
|
// matches, filtered by the caller. That's strictly no worse than the
|
|
40
|
-
//
|
|
40
|
+
// per-instance-cursor model (which already read every event
|
|
41
41
|
// from Postgres in every process) and sidesteps the subscribe-ack/
|
|
42
42
|
// first-publish race a per-channel scheme would have. Revisit with
|
|
43
43
|
// per-channel subscribe only if fanout volume becomes the bottleneck.
|
|
44
44
|
//
|
|
45
45
|
// Awaited by close() below — quitting while this is still in flight (a
|
|
46
46
|
// shutdown landing immediately after construction, e.g. a test closing
|
|
47
|
-
// right after building the signal) makes ioredis
|
|
47
|
+
// right after building the signal) makes ioredis reject the queued
|
|
48
48
|
// psubscribe with "Connection is closed" instead of it completing
|
|
49
49
|
// cleanly first. Not caught here or logged when a close is already
|
|
50
50
|
// underway (`closed`), so an intentional shutdown never surfaces this
|
|
@@ -54,8 +54,7 @@ export function createRedisPubSubSignal(opts) {
|
|
|
54
54
|
// this rejection is the expected side effect, not an ops-visible error.
|
|
55
55
|
if (closed)
|
|
56
56
|
return;
|
|
57
|
-
|
|
58
|
-
console.error(`[kumiko:${opts.label}] psubscribe failed:`, err);
|
|
57
|
+
log.error("psubscribe failed", { err });
|
|
59
58
|
});
|
|
60
59
|
// ioredis re-sends the psubscribe itself (autoResubscribe) but emits no ack
|
|
61
60
|
// event; repeating it here queues behind that one on the same connection,
|
|
@@ -77,8 +76,7 @@ export function createRedisPubSubSignal(opts) {
|
|
|
77
76
|
// skip: a close() already underway intentionally quit the connection
|
|
78
77
|
if (closed)
|
|
79
78
|
return;
|
|
80
|
-
|
|
81
|
-
console.error(`[kumiko:${opts.label}] resubscribe failed:`, err);
|
|
79
|
+
log.error("resubscribe failed", { err });
|
|
82
80
|
});
|
|
83
81
|
});
|
|
84
82
|
return {
|
|
@@ -88,8 +86,7 @@ export function createRedisPubSubSignal(opts) {
|
|
|
88
86
|
// connections — this rejection is the expected side effect.
|
|
89
87
|
if (closed)
|
|
90
88
|
return;
|
|
91
|
-
|
|
92
|
-
console.error(`[kumiko:${opts.label}] publish failed:`, err);
|
|
89
|
+
log.error("publish failed", { err });
|
|
93
90
|
});
|
|
94
91
|
},
|
|
95
92
|
async publishConfirmed(channel, payload) {
|
package/dist/schema-cli.js
CHANGED
|
@@ -17,8 +17,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
17
17
|
};
|
|
18
18
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join, resolve as resolvePath } from "node:path";
|
|
20
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
20
21
|
import { configureBlindIndexKey, configurePiiSubjectKms, resolveKmsWiringAsync, } from "./crypto/index.js";
|
|
21
22
|
import { assertUniqueTableNames, assertValidMigrationName, baselineMigrations, createDbConnection, diffReplayAgainstSnapshot, fetchAppliedMigrations, findCommentedDropTables, generateMigration, isRetiredFrameworkTable, loadMigrationsFromDir, loadSnapshotJson, readMigrationSqlTexts, rebuildTablesFromDiff, replayMigrationsDir, runMigrationsFromDir, tableExists, writeRebuildMarker, writeSnapshotJson, } from "./db/index.js";
|
|
23
|
+
import { isEntityTableMeta } from "./db/table-introspection.js";
|
|
22
24
|
import { validateBoot } from "./engine/boot-validator.js";
|
|
23
25
|
import { FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, } from "./engine/extension-names.js";
|
|
24
26
|
import { createRegistry } from "./engine/registry.js";
|
|
@@ -75,6 +77,79 @@ function parseValidateBootOptions(raw) {
|
|
|
75
77
|
...(warnOnUniqueAccessRoles !== undefined && { warnOnUniqueAccessRoles }),
|
|
76
78
|
};
|
|
77
79
|
}
|
|
80
|
+
function tableNameHint(entry) {
|
|
81
|
+
if (isPlainObject(entry) && typeof entry["tableName"] === "string")
|
|
82
|
+
return entry["tableName"];
|
|
83
|
+
return "unknown";
|
|
84
|
+
}
|
|
85
|
+
// Every non-optional top-level property of FeatureDefinition, grouped by the runtime shape the
|
|
86
|
+
// boot validators rely on; a hand-built entry missing one would crash far from the cause.
|
|
87
|
+
const FEATURE_ARRAY_KEYS = [
|
|
88
|
+
"requires",
|
|
89
|
+
"optionalRequires",
|
|
90
|
+
"configSeeds",
|
|
91
|
+
"extensionUsages",
|
|
92
|
+
"extensionSelectors",
|
|
93
|
+
"bootChecks",
|
|
94
|
+
"referenceData",
|
|
95
|
+
"configReads",
|
|
96
|
+
"authClaimsHooks",
|
|
97
|
+
];
|
|
98
|
+
const FEATURE_SET_KEYS = [
|
|
99
|
+
"requiredProjections",
|
|
100
|
+
"requiredSteps",
|
|
101
|
+
"exposedApis",
|
|
102
|
+
"usedApis",
|
|
103
|
+
];
|
|
104
|
+
const FEATURE_RECORD_KEYS = [
|
|
105
|
+
"relations",
|
|
106
|
+
"writeHandlers",
|
|
107
|
+
"queryHandlers",
|
|
108
|
+
"streamHandlers",
|
|
109
|
+
"translations",
|
|
110
|
+
"configKeys",
|
|
111
|
+
"jobs",
|
|
112
|
+
"registrarExtensions",
|
|
113
|
+
"notifications",
|
|
114
|
+
"events",
|
|
115
|
+
"eventMigrations",
|
|
116
|
+
"handlerEntityMappings",
|
|
117
|
+
"metrics",
|
|
118
|
+
"secretKeys",
|
|
119
|
+
"secretNamespaces",
|
|
120
|
+
"projections",
|
|
121
|
+
"multiStreamProjections",
|
|
122
|
+
"claimKeys",
|
|
123
|
+
"screens",
|
|
124
|
+
"navs",
|
|
125
|
+
"workspaces",
|
|
126
|
+
"httpRoutes",
|
|
127
|
+
"webSocketRoutes",
|
|
128
|
+
"storeTables",
|
|
129
|
+
];
|
|
130
|
+
function isFeatureDefinitionShape(entry) {
|
|
131
|
+
if (!isPlainObject(entry))
|
|
132
|
+
return false;
|
|
133
|
+
return (typeof entry["name"] === "string" &&
|
|
134
|
+
typeof entry["systemScope"] === "boolean" &&
|
|
135
|
+
FEATURE_ARRAY_KEYS.every((key) => Array.isArray(entry[key])) &&
|
|
136
|
+
FEATURE_SET_KEYS.every((key) => entry[key] instanceof Set) &&
|
|
137
|
+
FEATURE_RECORD_KEYS.every((key) => isPlainObject(entry[key])));
|
|
138
|
+
}
|
|
139
|
+
// FEATURES comes from the app's schema module; one malformed entry would make
|
|
140
|
+
// validateBoot fail with an unrelated error far from the cause, so name the index here.
|
|
141
|
+
function featureDefinitionsOf(value) {
|
|
142
|
+
if (!Array.isArray(value))
|
|
143
|
+
return undefined;
|
|
144
|
+
const definitions = [];
|
|
145
|
+
for (const [index, entry] of value.entries()) {
|
|
146
|
+
if (!isFeatureDefinitionShape(entry)) {
|
|
147
|
+
throw new Error(`FEATURES[${index}] is not a FeatureDefinition (expected an object with a string \`name\`)`);
|
|
148
|
+
}
|
|
149
|
+
definitions.push(entry);
|
|
150
|
+
}
|
|
151
|
+
return definitions;
|
|
152
|
+
}
|
|
78
153
|
// Single load-path for kumiko/schema.ts (512/3) — `generate` and `validate`
|
|
79
154
|
// both need ENTITY_METAS (and validate additionally wants FEATURES); this is
|
|
80
155
|
// the one place that enforces "ENTITY_METAS must be an array" so the two
|
|
@@ -83,13 +158,20 @@ function parseValidateBootOptions(raw) {
|
|
|
83
158
|
async function loadSchemaModFromApp(schemaFile) {
|
|
84
159
|
// bun imports TS directly — no spawn needed.
|
|
85
160
|
const mod = await import(__rewriteRelativeImportExtension(schemaFile));
|
|
86
|
-
|
|
161
|
+
const entityMetas = mod["ENTITY_METAS"];
|
|
162
|
+
if (!Array.isArray(entityMetas)) {
|
|
87
163
|
throw new Error(`Schema file ${schemaFile} muss \`export const ENTITY_METAS: EntityTableMeta[]\` haben.`);
|
|
88
164
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
165
|
+
const validMetas = [];
|
|
166
|
+
for (const [index, entry] of entityMetas.entries()) {
|
|
167
|
+
if (!isEntityTableMeta(entry)) {
|
|
168
|
+
throw new Error(`Schema file ${schemaFile}: ENTITY_METAS[${index}] is not an EntityTableMeta (table: ${tableNameHint(entry)}).`);
|
|
169
|
+
}
|
|
170
|
+
validMetas.push(entry);
|
|
171
|
+
}
|
|
172
|
+
assertUniqueTableNames(validMetas);
|
|
91
173
|
return {
|
|
92
|
-
entityMetas,
|
|
174
|
+
entityMetas: validMetas,
|
|
93
175
|
features: mod["FEATURES"],
|
|
94
176
|
validateBootOptions: mod["VALIDATE_BOOT_OPTIONS"],
|
|
95
177
|
};
|
|
@@ -223,9 +305,9 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
223
305
|
mkdirSync(migrationsDir, { recursive: true });
|
|
224
306
|
writeFileSync(join(migrationsDir, result.filename), result.sqlContent);
|
|
225
307
|
writeSnapshotJson(snapshotPath, result.snapshot);
|
|
226
|
-
// Rebuild
|
|
227
|
-
// (prevSnapshot===null)
|
|
228
|
-
//
|
|
308
|
+
// Rebuild markers are written only for incremental migrations — the initial
|
|
309
|
+
// migration (prevSnapshot===null) only creates tables, so there are no historical
|
|
310
|
+
// events to replay.
|
|
229
311
|
const rebuildTables = prevSnapshot === null ? [] : rebuildTablesFromDiff(result.diff);
|
|
230
312
|
writeRebuildMarker(migrationsDir, result.filename, rebuildTables);
|
|
231
313
|
out.log("");
|
|
@@ -269,7 +351,6 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
269
351
|
out.err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
270
352
|
return 1;
|
|
271
353
|
}
|
|
272
|
-
const mod = { FEATURES: schemaMod.features };
|
|
273
354
|
let ok = true;
|
|
274
355
|
// 1. Schema drift — compute the diff, never write.
|
|
275
356
|
const metas = schemaMod.entityMetas;
|
|
@@ -299,9 +380,21 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
299
380
|
out.err(" Fix: `kumiko-schema generate <name>`, then commit the migration.");
|
|
300
381
|
}
|
|
301
382
|
// 2. Boot validity — needs the composed feature set.
|
|
302
|
-
|
|
383
|
+
let bootFeatures;
|
|
384
|
+
let bootFeaturesError;
|
|
385
|
+
try {
|
|
386
|
+
bootFeatures = featureDefinitionsOf(schemaMod.features);
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
bootFeaturesError = e instanceof Error ? e.message : String(e);
|
|
390
|
+
}
|
|
391
|
+
if (bootFeaturesError !== undefined) {
|
|
392
|
+
ok = false;
|
|
393
|
+
out.err(` ✗ boot: ${bootFeaturesError}`);
|
|
394
|
+
}
|
|
395
|
+
else if (bootFeatures) {
|
|
303
396
|
try {
|
|
304
|
-
validateBoot(
|
|
397
|
+
validateBoot(bootFeatures, {
|
|
305
398
|
...parseValidateBootOptions(schemaMod.validateBootOptions),
|
|
306
399
|
env: withFileStoragePresence(process.env),
|
|
307
400
|
});
|
|
@@ -350,7 +443,7 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
350
443
|
// replayMigrationsDir fail-loud's on a table-DDL statement it can't
|
|
351
444
|
// parse — that must surface as a normal ✗ line like every other
|
|
352
445
|
// check here, not a raw stack trace that kills the process before
|
|
353
|
-
// any later check (or the Fix: help) ever runs
|
|
446
|
+
// any later check (or the Fix: help) ever runs.
|
|
354
447
|
ok = false;
|
|
355
448
|
out.err(` ✗ migration-content: ${e instanceof Error ? e.message : String(e)}`);
|
|
356
449
|
out.err(" Fix: a statement in a migration file isn't recognizable to the replay parser — extend the parser's recognized patterns, or reword the migration to one it understands.");
|
|
@@ -372,11 +465,12 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
372
465
|
let wiring;
|
|
373
466
|
try {
|
|
374
467
|
const result = await runMigrationsFromDir(db, migrationsDir);
|
|
375
|
-
// Framework
|
|
376
|
-
// `generate
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
468
|
+
// Framework infrastructure tables (event store + pipeline state) are not captured by
|
|
469
|
+
// `generate`, which handles only entity read tables. Existing databases have them
|
|
470
|
+
// from the legacy-drizzle foundation; without them, a greenfield database (the
|
|
471
|
+
// first app without a cutover) would lack kumiko_events and runProdApp would fail
|
|
472
|
+
// during boot. All three are idempotent (tableExists-gated), so they are no-ops for
|
|
473
|
+
// existing databases.
|
|
380
474
|
await createEventsTable(db);
|
|
381
475
|
await createEventConsumerStateTable(db);
|
|
382
476
|
await createProjectionStateTable(db);
|
|
@@ -400,14 +494,14 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
400
494
|
// (kumiko_pending_rebuilds) survives a failed/crashed rebuild from an
|
|
401
495
|
// earlier apply, so a later apply with zero new migrations still must
|
|
402
496
|
// retry it — otherwise a failed rebuild is silently never retried,
|
|
403
|
-
// since the migration itself is already tracked applied
|
|
497
|
+
// since the migration itself is already tracked applied.
|
|
404
498
|
if (options.features) {
|
|
405
499
|
// A rebuild replays applyEntityEvent, which computes bidx columns from
|
|
406
500
|
// configureBlindIndexKey/configurePiiSubjectKms — both boot-injected by
|
|
407
501
|
// run{Prod,Dev}App but never by this standalone CLI (the migrate-db
|
|
408
502
|
// initContainer). Without wiring them here, a rebuild would silently
|
|
409
503
|
// null out bidx columns and, for ciphertext, throw only once it hits
|
|
410
|
-
// the row
|
|
504
|
+
// the row.
|
|
411
505
|
wiring = await resolveKmsWiringAsync(process.env, {
|
|
412
506
|
logPrefix: "[kumiko schema apply]",
|
|
413
507
|
log: out.log,
|
|
@@ -485,10 +579,10 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
485
579
|
const local = loadMigrationsFromDir(migrationsDir);
|
|
486
580
|
const { db, close } = createDbConnection({ url: dbUrl });
|
|
487
581
|
try {
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
582
|
+
// On a fresh database where `kumiko schema apply` has never run, the tracking table is
|
|
583
|
+
// absent, which means "nothing applied". Connection and permission errors must not
|
|
584
|
+
// be swallowed (false pending results would hide the problem) — tableExists checks
|
|
585
|
+
// only for existence, while all other errors propagate.
|
|
492
586
|
const trackingExists = await tableExists(db, "_kumiko_migrations");
|
|
493
587
|
const applied = trackingExists
|
|
494
588
|
? new Set((await fetchAppliedMigrations(db)).map((a) => a.id))
|
package/dist/search/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Meilisearch
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The Meilisearch adapter is exposed through the subpath export
|
|
2
|
+
// `@cosmicdrift/kumiko-framework/search/meilisearch`. This keeps the main barrel from loading a
|
|
3
|
+
// Meilisearch client when consumers only use SearchAdapter types, so apps that do not use
|
|
4
|
+
// Meilisearch do not pull in the client code.
|
|
5
5
|
export { collectSearchableFieldNames, configureTenantSearchIndex, } from "./configure-tenant-index.js";
|
|
6
6
|
export { deriveSearchAdapterConfig } from "./derive-search-adapter-config.js";
|
|
7
7
|
export { createInMemorySearchAdapter } from "./in-memory-adapter.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Purge derived search documents for an erased PII subject
|
|
1
|
+
// Purge derived search documents for an erased PII subject.
|
|
2
2
|
//
|
|
3
3
|
// After kms.eraseKey the projection/event ciphertext is unreadable, but Meili
|
|
4
4
|
// still holds the plaintext that createSearchEventConsumer decrypted into the
|
|
@@ -152,7 +152,7 @@ subject) {
|
|
|
152
152
|
if (declaredFields.length === 0)
|
|
153
153
|
continue;
|
|
154
154
|
const tableName = resolveTableName(entityName, entity, undefined);
|
|
155
|
-
// Same post-eraseKey hazard as nullBlindIndexesForSubject
|
|
155
|
+
// Same post-eraseKey hazard as nullBlindIndexesForSubject:
|
|
156
156
|
// a mounted feature without its migration must not abort the purge
|
|
157
157
|
// (and the audit event after it).
|
|
158
158
|
if (!(await tableExists(db, tableName)))
|
|
@@ -178,13 +178,12 @@ subject) {
|
|
|
178
178
|
for (const [tenantId, items] of byTenant) {
|
|
179
179
|
if (items.length === 0)
|
|
180
180
|
continue;
|
|
181
|
-
const tid = tenantId;
|
|
182
181
|
if (search.removeBatch) {
|
|
183
|
-
await search.removeBatch(
|
|
182
|
+
await search.removeBatch(tenantId, items);
|
|
184
183
|
}
|
|
185
184
|
else {
|
|
186
185
|
for (const item of items) {
|
|
187
|
-
await search.remove(
|
|
186
|
+
await search.remove(tenantId, item.entityType, item.entityId);
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
189
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// One-time backfill for entities that only got `searchable: true` after rows
|
|
2
|
-
// already existed
|
|
2
|
+
// already existed. buildSearchDocument() only runs on the write path
|
|
3
3
|
// (createSearchEventConsumer, system-hooks.ts) — pre-existing rows never
|
|
4
4
|
// pass through it, so they stay unfindable until their next write. This
|
|
5
5
|
// re-derives a SearchDocument straight from the read-table row for every
|
|
@@ -47,12 +47,8 @@ async function scanAndIndexEntity(db, registry, searchAdapter, entityName, tenan
|
|
|
47
47
|
// calls searchAdapter.remove()) — resurrecting them here would make
|
|
48
48
|
// erased entities findable again.
|
|
49
49
|
const deletedFilter = entity.softDelete === true ? `AND ${quoteIdent("is_deleted")} IS NOT TRUE` : "";
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
indexedRows: 0,
|
|
53
|
-
wouldIndexRows: 0,
|
|
54
|
-
failures: [],
|
|
55
|
-
};
|
|
50
|
+
const failures = [];
|
|
51
|
+
const result = { scannedRows: 0, indexedRows: 0, wouldIndexRows: 0, failures };
|
|
56
52
|
let lastId;
|
|
57
53
|
// Whether a searchable field lacks a matching read-table column depends
|
|
58
54
|
// only on the table's column set (rowToState/Object.hasOwn), not on any
|
|
@@ -69,11 +65,12 @@ async function scanAndIndexEntity(db, registry, searchAdapter, entityName, tenan
|
|
|
69
65
|
WHERE ${quoteIdent("tenant_id")} = $1 ${deletedFilter} ${afterLastId}
|
|
70
66
|
ORDER BY ${quoteIdent("id")} ASC
|
|
71
67
|
LIMIT $2`, lastId === undefined ? [tenantId, batchSize] : [tenantId, batchSize, lastId]);
|
|
72
|
-
|
|
68
|
+
const [firstRow] = rows;
|
|
69
|
+
if (firstRow === undefined)
|
|
73
70
|
break;
|
|
74
71
|
if (uncheckedSchema) {
|
|
75
72
|
uncheckedSchema = false;
|
|
76
|
-
const sampleState = rowToState(
|
|
73
|
+
const sampleState = rowToState(firstRow, fieldNames);
|
|
77
74
|
const unmappedField = searchableFields.find((fieldName) => !(fieldName in sampleState));
|
|
78
75
|
if (unmappedField !== undefined) {
|
|
79
76
|
throw new Error(`reindexEntity: searchable field "${unmappedField}" is not mappable from read-table ` +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Warn once at server startup when the process time zone is not UTC.
|
|
3
|
+
* Parameters are injectable for tests. Returns whether a warning was emitted.
|
|
4
4
|
*/
|
|
5
5
|
export declare function warnIfNonUtcServerTimeZone(resolvedTimeZone?: string, warn?: (message: string) => void): boolean;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Warn at boot when the process time zone is not UTC. All server code assumes UTC
|
|
2
|
+
// (the system time zone is not a framework concept); a different process time zone
|
|
3
|
+
// can make accidental new Date() paths behave differently locally ("green in UTC-CI,
|
|
4
|
+
// broken in Berlin production"). This is a soft check: non-UTC is legitimate in
|
|
5
|
+
// development, so warn instead of failing boot.
|
|
6
6
|
function resolvedServerTimeZone() {
|
|
7
7
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Warn once at server startup when the process time zone is not UTC.
|
|
11
|
+
* Parameters are injectable for tests. Returns whether a warning was emitted.
|
|
12
12
|
*/
|
|
13
13
|
export function warnIfNonUtcServerTimeZone(resolvedTimeZone = resolvedServerTimeZone(),
|
|
14
|
-
// biome-ignore lint/suspicious/noConsole:
|
|
14
|
+
// biome-ignore lint/suspicious/noConsole: runs before any logger exists; the default is the only console sink
|
|
15
15
|
warn = console.warn) {
|
|
16
16
|
// GMT and Etc/UTC are UTC-equivalent (no offset, no DST) — TZ=GMT is a
|
|
17
17
|
// legitimate way to pin a process to UTC and must not trip this warning.
|