@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import { Hono } from "hono";
|
|
2
3
|
import { bodyLimit } from "hono/body-limit";
|
|
3
4
|
import { getUser } from "../api/auth-middleware.js";
|
|
@@ -18,6 +19,12 @@ import { RecordWriteDecisions } from "./record-write-check.js";
|
|
|
18
19
|
import { neutralFileNameForMimeType } from "./resolve-content-type.js";
|
|
19
20
|
import { parseMaxSize, resolveServedContentType } from "./types.js";
|
|
20
21
|
import { createFileUploadLedger } from "./upload-ledger.js";
|
|
22
|
+
// Exactly the httpStatus values declared by KumikoError subclasses across packages/*.
|
|
23
|
+
const ERROR_STATUSES = [400, 401, 403, 404, 409, 422, 429, 500, 503, 504];
|
|
24
|
+
export function deleteFailureResponseFor(error) {
|
|
25
|
+
const status = isOneOf(ERROR_STATUSES, error.httpStatus) ? error.httpStatus : 500;
|
|
26
|
+
return { status, code: status === 404 ? "not_found" : error.code };
|
|
27
|
+
}
|
|
21
28
|
const DEFAULT_PRIVILEGED_ROLES = ["Admin", "SystemAdmin"];
|
|
22
29
|
// 15 minutes — long enough for a download to start, short enough that a
|
|
23
30
|
// leaked URL (e.g. from a browser history screenshot) isn't a long-lived
|
|
@@ -281,7 +288,7 @@ export function createFileRoutes(options) {
|
|
|
281
288
|
// GET /files/:id/variant/:name — a named derived version of the file.
|
|
282
289
|
// Same tenant + guard gate as the download above; the original stays
|
|
283
290
|
// reachable only through that route. The public, anonymous counterpart is
|
|
284
|
-
// a separate cut
|
|
291
|
+
// a separate cut and does not go through createFileRoutes.
|
|
285
292
|
api.get("/files/:id/variant/:name", async (c) => {
|
|
286
293
|
const user = getUser(c);
|
|
287
294
|
const id = c.req.param("id");
|
|
@@ -358,9 +365,8 @@ export function createFileRoutes(options) {
|
|
|
358
365
|
// pattern from the access-deny path above. Everything else (version
|
|
359
366
|
// conflict 409, ownership 403, validation 422, internal 500) gets the
|
|
360
367
|
// real status off the error so callers can distinguish recoverable
|
|
361
|
-
// from terminal —
|
|
362
|
-
const status = result.error
|
|
363
|
-
const code = status === 404 ? "not_found" : result.error.code;
|
|
368
|
+
// from terminal — a blanket 404 here had obscured exactly that.
|
|
369
|
+
const { status, code } = deleteFailureResponseFor(result.error);
|
|
364
370
|
return c.json({ error: code }, status);
|
|
365
371
|
}
|
|
366
372
|
return c.json({ ok: true });
|
|
@@ -411,7 +417,7 @@ export function createFileRoutes(options) {
|
|
|
411
417
|
return c.json({ url, expiresAt });
|
|
412
418
|
});
|
|
413
419
|
// Metadata without the bytes. Unlike the byte-serving routes, this returns
|
|
414
|
-
// fileName as JSON — and fileName is `personal: "self"
|
|
420
|
+
// fileName as JSON — and fileName is `personal: "self"`, so it must
|
|
415
421
|
// go through the entity executor's detail() to decrypt instead of
|
|
416
422
|
// loadFileForTenant's raw, still-encrypted read.
|
|
417
423
|
async function loadReadableFileMeta(id, user) {
|
|
@@ -12,9 +12,8 @@ export function createInMemoryFileProvider() {
|
|
|
12
12
|
store.set(key, { data: new Uint8Array(data), mimeType });
|
|
13
13
|
},
|
|
14
14
|
async writeStream(key, source, options) {
|
|
15
|
-
// In-
|
|
16
|
-
//
|
|
17
|
-
// Production-Pfad.
|
|
15
|
+
// In-memory storage does not provide real streaming; it collects chunks
|
|
16
|
+
// into a Uint8Array. Suitable for tests, not a production path.
|
|
18
17
|
const chunks = [];
|
|
19
18
|
let total = 0;
|
|
20
19
|
for await (const chunk of source) {
|
|
@@ -36,12 +35,12 @@ export function createInMemoryFileProvider() {
|
|
|
36
35
|
return new Uint8Array(entry.data);
|
|
37
36
|
},
|
|
38
37
|
readStream(key) {
|
|
39
|
-
// In-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
38
|
+
// In-memory storage has no chunks, but the surface must match real
|
|
39
|
+
// streamers (Local/S3) so test code exercises the same path. Yield the
|
|
40
|
+
// bytes as a single chunk.
|
|
41
|
+
// The O(1) map lookup avoids a cost from eager resolution.
|
|
42
|
+
// Errors are thrown on the first chunk pull, matching S3's lazy pattern
|
|
43
|
+
// (the request starts on the first iterator step).
|
|
45
44
|
const entry = store.get(key);
|
|
46
45
|
const captured = entry ? new Uint8Array(entry.data) : null;
|
|
47
46
|
return {
|
|
@@ -33,16 +33,12 @@ export function createLocalProvider(basePath) {
|
|
|
33
33
|
await writeFile(filePath, data);
|
|
34
34
|
},
|
|
35
35
|
async writeStream(key, source, _options) {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// Problem (kein Reader sucht `*.tmp`-Patterns), nur Operations-
|
|
43
|
-
// Hygiene; ein periodischer cron-cleanup auf alten `.tmp`-Files
|
|
44
|
-
// ist die saubere Loesung wenn das in Production realistisch
|
|
45
|
-
// greift.
|
|
36
|
+
// Atomic via a temporary file and rename: readers see either the previous version (if any) or
|
|
37
|
+
// the complete new version, never a partially written stream. If the stream fails during
|
|
38
|
+
// writing, temporary-file cleanup is best-effort; an OS race in stream destruction can leave
|
|
39
|
+
// the `.tmp` file briefly. This affects operational hygiene only, because readers never
|
|
40
|
+
// search `*.tmp` patterns. A periodic cleanup of old `.tmp` files is appropriate if this runs
|
|
41
|
+
// in production.
|
|
46
42
|
const filePath = resolveContainedPath(key);
|
|
47
43
|
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
48
44
|
await mkdir(dirname(filePath), { recursive: true });
|
|
@@ -51,8 +47,8 @@ export function createLocalProvider(basePath) {
|
|
|
51
47
|
await rename(tmpPath, filePath);
|
|
52
48
|
}
|
|
53
49
|
catch (e) {
|
|
54
|
-
// Best-effort
|
|
55
|
-
//
|
|
50
|
+
// Best-effort temporary-file cleanup; if that also fails, the filesystem problem is outside
|
|
51
|
+
// this component's scope.
|
|
56
52
|
await unlink(tmpPath).catch(() => { });
|
|
57
53
|
throw e;
|
|
58
54
|
}
|
|
@@ -62,24 +58,21 @@ export function createLocalProvider(basePath) {
|
|
|
62
58
|
return readFile(filePath);
|
|
63
59
|
},
|
|
64
60
|
readStream(key) {
|
|
65
|
-
// node:fs createReadStream
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// (z.B. ENOENT bei Missing-File faellt erst beim ersten chunk-
|
|
72
|
-
// pull, nicht beim readStream-Aufruf — gleiches Lazy-Verhalten
|
|
73
|
-
// wie inmemory + S3).
|
|
61
|
+
// node:fs createReadStream is an AsyncIterable<Buffer>; Buffer extends Uint8Array. This
|
|
62
|
+
// adapter preserves a Uint8Array surface because @types/node types the async iterator as
|
|
63
|
+
// AsyncIterableIterator<any>. The default highWaterMark produces 64KB chunks, which suits ZIP
|
|
64
|
+
// stream consumption. Errors reach the consumer's for-await loop; for example, a missing file
|
|
65
|
+
// raises ENOENT on the first chunk pull rather than when readStream is called, matching the
|
|
66
|
+
// lazy behavior of in-memory and S3 providers.
|
|
74
67
|
const filePath = resolveContainedPath(key);
|
|
75
68
|
const stream = createReadStream(filePath);
|
|
76
69
|
return {
|
|
77
70
|
async *[Symbol.asyncIterator]() {
|
|
78
71
|
for await (const chunk of stream) {
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
72
|
+
// A stream without an encoding yields Buffer. Buffer extends Uint8Array, but
|
|
73
|
+
// @types/node types the async iterator as string|Buffer. Take a zero-copy view
|
|
74
|
+
// onto the same ArrayBuffer; the runtime check rules out the string branch
|
|
75
|
+
// because the stream was not opened with an encoding.
|
|
83
76
|
if (typeof chunk === "string") {
|
|
84
77
|
throw new Error("local-provider readStream: unexpected string chunk (encoding leaked)");
|
|
85
78
|
}
|
|
@@ -36,13 +36,13 @@ export type FileProviderContext = {
|
|
|
36
36
|
* file-provider-inmemory, ...) registers an implementation via
|
|
37
37
|
* `r.useExtension(EXT_FILE_PROVIDER, "<name>", { build })`.
|
|
38
38
|
*
|
|
39
|
-
* **Plugin-Author-
|
|
40
|
-
*
|
|
41
|
-
* `ctx.tx`, `ctx.actor`, `ctx.signal`)
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* **Plugin-Author-Warning:** `ctx` is EXPLICITLY a FileProviderContext, not
|
|
40
|
+
* a full HandlerContext. Fields outside the narrow surface (e.g.
|
|
41
|
+
* `ctx.tx`, `ctx.actor`, `ctx.signal`) are NOT available on the worker path.
|
|
42
|
+
* Casting `ctx as unknown as HandlerContext` fails at runtime on the worker —
|
|
43
|
+
* the crash only appears in production with the first S3 tenant. If a plugin
|
|
44
|
+
* needs fields outside FileProviderContext, extend FileProviderContext
|
|
45
|
+
* explicitly (a visible breaking change) rather than casting `ctx`.
|
|
46
46
|
*/
|
|
47
47
|
export type FileProviderPlugin = {
|
|
48
48
|
readonly build: (ctx: FileProviderContext, tenantId: string) => Promise<FileStorageProvider>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Tenant storage usage — counts bytes + files per tenant from the event log.
|
|
2
2
|
//
|
|
3
|
-
// Tracking-only
|
|
4
|
-
//
|
|
3
|
+
// Tracking-only: no hard limit, no upload gatekeeping. Apps read the row to
|
|
4
|
+
// decide what to do (show a warning, soft-throttle, bill, …).
|
|
5
5
|
// Enforcement is a conscious deferred call — we want production numbers
|
|
6
6
|
// before picking thresholds (see "Storage tracking: counted now, enforced
|
|
7
7
|
// later" in core-files.md).
|
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
import { getTemporal } from "../time/index.ts";
|
|
2
2
|
export interface ZipEntry {
|
|
3
|
-
/**
|
|
3
|
+
/** Path inside the ZIP, slash-separated and UTF-8 encoded (general-purpose flag bit 11). */
|
|
4
4
|
readonly path: string;
|
|
5
|
-
/** Body
|
|
5
|
+
/** Body as an AsyncIterable; it may be generated lazily to avoid upfront memory use. */
|
|
6
6
|
readonly data: AsyncIterable<Uint8Array>;
|
|
7
7
|
/**
|
|
8
|
-
* Optional
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Optional modification time. Defaults to the current time and uses the ZIP
|
|
9
|
+
* MS-DOS format with two-second precision. Set the generation timestamp on
|
|
10
|
+
* every entry when consistent audit-trail timestamps are required.
|
|
11
11
|
*/
|
|
12
12
|
readonly mtime?: InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* `FileStorageProvider.writeStream
|
|
17
|
-
* Entry haengt im Memory bis fertig.
|
|
15
|
+
* Creates a ZIP stream as `AsyncIterable<Uint8Array>`. The caller, such as
|
|
16
|
+
* `FileStorageProvider.writeStream`, consumes the output chunk by chunk.
|
|
18
17
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Trade-off: 1 Entry-Body im Memory at-a-time; bei <100 MB Files
|
|
25
|
-
* OK, bei >1 GB single-Files braeuchte ZIP64+Streaming-Descriptor).
|
|
26
|
-
* 2. File Body (raw bytes)
|
|
27
|
-
* Am Ende:
|
|
28
|
-
* 3. Central Directory (eine Liste-Entry pro File mit CRC + size)
|
|
29
|
-
* 4. EOCD (End Of Central Directory)
|
|
18
|
+
* Each entry's body is collected in memory before its local file header is
|
|
19
|
+
* emitted because non-seekable storage providers cannot use a streaming data
|
|
20
|
+
* descriptor, and the header must contain the CRC32 and size. This limits
|
|
21
|
+
* memory use to one entry body at a time; ZIP64 plus streaming descriptors
|
|
22
|
+
* would be needed for larger single files.
|
|
30
23
|
*/
|
|
31
24
|
export declare function createZipStream(entries: AsyncIterable<ZipEntry>): AsyncIterable<Uint8Array>;
|
package/dist/files/zip-stream.js
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import { getTemporal } from "../time/index.js";
|
|
2
|
-
//
|
|
2
|
+
// Pure-JS streaming ZIP builder without dependencies.
|
|
3
3
|
//
|
|
4
|
-
// **Format
|
|
5
|
-
//
|
|
6
|
-
// -
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// - DEFLATE braucht zlib + buffering pro Entry, killt das Streaming-
|
|
10
|
-
// Property
|
|
4
|
+
// **Format: STORE (no DEFLATE).** Avoiding compression keeps the implementation minimal:
|
|
5
|
+
// - Storage providers (S3/R2) compress data on the wire (HTTP gzip).
|
|
6
|
+
// - JSON snippets are small, and file binaries are often already compressed
|
|
7
|
+
// (PNG/JPG/PDF/MP4/...).
|
|
8
|
+
// - DEFLATE requires zlib and per-entry buffering, which defeats streaming.
|
|
11
9
|
//
|
|
12
|
-
// **Streaming
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// ist. Storage-Provider's writeStream konsumiert direkt durch.
|
|
10
|
+
// **Streaming:** The caller provides `AsyncIterable<ZipEntry>`, and each entry
|
|
11
|
+
// has `data: AsyncIterable<Uint8Array>`. ZIP bytes are yielded chunk by chunk.
|
|
12
|
+
// The storage provider's writeStream consumes them directly.
|
|
16
13
|
//
|
|
17
|
-
// **ZIP
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// in unserem Use-Case: "profile.json", "files/<id>.<ext>").
|
|
14
|
+
// **ZIP standard:** APPNOTE.TXT v6.3.10 (April 2022). STORE method, no ZIP64
|
|
15
|
+
// (maximum 4 GB per entry and 65535 entries). ZIP64 support is needed before
|
|
16
|
+
// exporting a single file larger than 4 GB. The UTF-8 flag is enabled because
|
|
17
|
+
// filenames may contain non-ASCII characters.
|
|
22
18
|
//
|
|
23
|
-
// **CRC32:**
|
|
24
|
-
// einmal initialisiert + cached.
|
|
19
|
+
// **CRC32:** Classic IEEE-802.3 polynomial (0xEDB88320), with a cached lookup table.
|
|
25
20
|
const ZIP_VERSION_NEEDED = 20; // 2.0 = STORE
|
|
26
21
|
const ZIP_METHOD_STORE = 0;
|
|
27
22
|
// Local file header signature: 0x04034b50 = "PK\x03\x04"
|
|
@@ -31,50 +26,38 @@ const CENTRAL_DIR_HEADER_SIG = 0x02014b50;
|
|
|
31
26
|
// End of central directory record signature: 0x06054b50 = "PK\x05\x06"
|
|
32
27
|
const EOCD_SIG = 0x06054b50;
|
|
33
28
|
const VERSION_MADE_BY = 0x031e; // 0x03 = UNIX, 0x1e = ZIP 3.0
|
|
34
|
-
// General Purpose Flag Bit 11 (0x0800)
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// Umlauten ("Bügel.pdf") wird das Mojibake.
|
|
29
|
+
// General Purpose Flag Bit 11 (0x0800) enables UTF-8 encoding for filenames and comments.
|
|
30
|
+
// ASCII is valid UTF-8, while omitting the flag makes older ZIP tools interpret
|
|
31
|
+
// filenames as CP437, producing mojibake for non-ASCII characters such as umlauts.
|
|
38
32
|
const GENERAL_PURPOSE_FLAGS = 0x0800;
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// Bits entpackt Info-ZIP mit Mode 0000 (EACCES beim Read).
|
|
33
|
+
// Store UNIX file permissions in the high bytes of the external attributes.
|
|
34
|
+
// Without `(S_IFREG | 0644) << 16 = 0o100644 << 16 = 0x81a40000`, Info-ZIP
|
|
35
|
+
// extracts regular files with mode 0000, causing EACCES when they are read.
|
|
43
36
|
const UNIX_REGULAR_FILE_0644 = 0o100644 << 16; // 0x81a40000
|
|
44
|
-
//
|
|
45
|
-
// ZIP64
|
|
37
|
+
// STORE without ZIP64 is limited to 4 GB per entry and 65535 entries.
|
|
38
|
+
// ZIP64 support is required before these limits can be exceeded.
|
|
46
39
|
const ZIP_MAX_ENTRY_SIZE = 0xffffffff; // uint32 → 4 GB
|
|
47
40
|
const ZIP_MAX_ENTRIES = 0xffff; // uint16 → 65535
|
|
48
41
|
/**
|
|
49
|
-
*
|
|
50
|
-
* `FileStorageProvider.writeStream
|
|
51
|
-
* Entry haengt im Memory bis fertig.
|
|
42
|
+
* Creates a ZIP stream as `AsyncIterable<Uint8Array>`. The caller, such as
|
|
43
|
+
* `FileStorageProvider.writeStream`, consumes the output chunk by chunk.
|
|
52
44
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Trade-off: 1 Entry-Body im Memory at-a-time; bei <100 MB Files
|
|
59
|
-
* OK, bei >1 GB single-Files braeuchte ZIP64+Streaming-Descriptor).
|
|
60
|
-
* 2. File Body (raw bytes)
|
|
61
|
-
* Am Ende:
|
|
62
|
-
* 3. Central Directory (eine Liste-Entry pro File mit CRC + size)
|
|
63
|
-
* 4. EOCD (End Of Central Directory)
|
|
45
|
+
* Each entry's body is collected in memory before its local file header is
|
|
46
|
+
* emitted because non-seekable storage providers cannot use a streaming data
|
|
47
|
+
* descriptor, and the header must contain the CRC32 and size. This limits
|
|
48
|
+
* memory use to one entry body at a time; ZIP64 plus streaming descriptors
|
|
49
|
+
* would be needed for larger single files.
|
|
64
50
|
*/
|
|
65
51
|
export async function* createZipStream(entries) {
|
|
66
52
|
const centralDirRecords = [];
|
|
67
|
-
let offset = 0; //
|
|
53
|
+
let offset = 0; // Tracks the local-header offset for each central-directory entry
|
|
68
54
|
for await (const entry of entries) {
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// wollen vermeiden dass der konsumierende Storage zwei-pass laesst).
|
|
55
|
+
// Collect the body before writing the local file header: non-seekable storage
|
|
56
|
+
// providers cannot use a streaming data descriptor, and a two-pass read
|
|
57
|
+
// would be required otherwise.
|
|
73
58
|
const body = await collectBody(entry.data);
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// ZIP produzieren (Decoder lesen Muell-Sizes). Lieber early-fail mit
|
|
77
|
-
// klarer Begruendung — Worker (Atom 3b) fängt das + setzt Job=failed.
|
|
59
|
+
// STORE without ZIP64 uses uint32 sizes. Fail early instead of allowing integer
|
|
60
|
+
// wraparound to produce a corrupt ZIP with incorrect sizes.
|
|
78
61
|
if (body.byteLength > ZIP_MAX_ENTRY_SIZE) {
|
|
79
62
|
throw new Error(`ZIP-Entry "${entry.path}" exceeds 4 GB limit (${body.byteLength} bytes). ` +
|
|
80
63
|
`STORE-mode without ZIP64-extension caps at uint32. ` +
|
|
@@ -105,8 +88,7 @@ export async function* createZipStream(entries) {
|
|
|
105
88
|
lfh.set(filenameBytes, 30);
|
|
106
89
|
yield lfh;
|
|
107
90
|
yield body;
|
|
108
|
-
//
|
|
109
|
-
// emittieren ihn am Ende.
|
|
91
|
+
// Keep central-directory entries until all file bodies have been emitted.
|
|
110
92
|
const cdh = new Uint8Array(46 + filenameBytes.byteLength);
|
|
111
93
|
const cdhView = new DataView(cdh.buffer);
|
|
112
94
|
cdhView.setUint32(0, CENTRAL_DIR_HEADER_SIG, true);
|
|
@@ -130,7 +112,7 @@ export async function* createZipStream(entries) {
|
|
|
130
112
|
centralDirRecords.push(cdh);
|
|
131
113
|
offset += lfh.byteLength + body.byteLength;
|
|
132
114
|
}
|
|
133
|
-
// Central
|
|
115
|
+
// Central directory: all entries back to back.
|
|
134
116
|
const centralDirStart = offset;
|
|
135
117
|
let centralDirSize = 0;
|
|
136
118
|
for (const record of centralDirRecords) {
|
|
@@ -189,15 +171,14 @@ function crc32(data) {
|
|
|
189
171
|
}
|
|
190
172
|
return (crc ^ 0xffffffff) >>> 0;
|
|
191
173
|
}
|
|
192
|
-
// MS-DOS
|
|
193
|
-
// Date: bits 9-15=year-1980, 5-8=month, 0-4=day.
|
|
194
|
-
// Time: bits 11-15=hour, 5-10=minute, 0-4=second/2.
|
|
174
|
+
// MS-DOS date and time encoding for ZIP headers.
|
|
175
|
+
// Date: bits 9-15 = year - 1980, 5-8 = month, 0-4 = day.
|
|
176
|
+
// Time: bits 11-15 = hour, 5-10 = minute, 0-4 = second / 2.
|
|
195
177
|
//
|
|
196
|
-
// **UTC**
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// server-side-Timestamps im Repo (Temporal.Instant ueberall).
|
|
178
|
+
// **UTC** instead of the local time zone: in a GDPR context the ZIP mtime is
|
|
179
|
+
// part of the data-access export. Different server time zones would otherwise produce
|
|
180
|
+
// different values for the same generation instant, causing audit drift. UTC is
|
|
181
|
+
// used for server-side timestamps throughout the repository.
|
|
201
182
|
function toDosTime(i) {
|
|
202
183
|
const dt = i.toZonedDateTimeISO("UTC");
|
|
203
184
|
const year = dt.year;
|
|
@@ -16,7 +16,7 @@ export declare function booleanFacetOptionKeys(featureName: string, entityName:
|
|
|
16
16
|
export declare function selectFacetOptionKey(featureName: string, entityName: string, fieldName: string, value: string): string;
|
|
17
17
|
export declare function screenTitleKey(screenId: string): string;
|
|
18
18
|
export type RequiredKeysOptions = {
|
|
19
|
-
/** Bypass `isI18nKey`'s colon-only check for generated dot-form labels
|
|
19
|
+
/** Bypass `isI18nKey`'s colon-only check for generated dot-form labels. */
|
|
20
20
|
readonly treatDotFormAsKey?: boolean;
|
|
21
21
|
};
|
|
22
22
|
export declare function requiredKeysFromScreen(featureName: string, screen: ScreenDefinition, options?: RequiredKeysOptions): readonly string[];
|
|
@@ -35,8 +35,8 @@ function isI18nKey(value) {
|
|
|
35
35
|
/**
|
|
36
36
|
* `treatAsKey` bypasses the colon-only `isI18nKey` check — used by the Settings-Hub
|
|
37
37
|
* generator's dot-form labels (`${feature}.settings`, mask titles), which are
|
|
38
|
-
* always i18n references by construction, never literal display text
|
|
39
|
-
* `isI18nKey` itself also accepts dot-form labels explicitly marked via `i18nKey()
|
|
38
|
+
* always i18n references by construction, never literal display text.
|
|
39
|
+
* `isI18nKey` itself also accepts dot-form labels explicitly marked via `i18nKey()`.
|
|
40
40
|
*/
|
|
41
41
|
function pushKey(out, value, treatAsKey = false) {
|
|
42
42
|
if (value !== undefined && (treatAsKey || isI18nKey(value)))
|
|
@@ -112,7 +112,7 @@ function pushDashboardScreenKeys(out, dashboard) {
|
|
|
112
112
|
pushRowActionKeys(out, action);
|
|
113
113
|
}
|
|
114
114
|
function pushDashboardPanelKeys(out, panel) {
|
|
115
|
-
// skip: custom
|
|
115
|
+
// skip: custom panels translate themselves, so no key is collected here
|
|
116
116
|
if (panel.kind === "custom")
|
|
117
117
|
return;
|
|
118
118
|
pushKey(out, panel.label);
|
|
@@ -251,7 +251,8 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
251
251
|
}
|
|
252
252
|
case "projectionList": {
|
|
253
253
|
const list = screen;
|
|
254
|
-
//
|
|
254
|
+
// No entity means there are no field-label fallbacks; only explicit column labels are
|
|
255
|
+
// collected.
|
|
255
256
|
for (const col of list.columns) {
|
|
256
257
|
const normalized = normalizeListColumn(col);
|
|
257
258
|
if (normalized.label !== undefined)
|
|
@@ -454,9 +455,8 @@ function collectEntityListFilterKeys(feature, out) {
|
|
|
454
455
|
const entity = feature.entities?.[screen.entity];
|
|
455
456
|
if (!entity)
|
|
456
457
|
continue;
|
|
457
|
-
for (const [fieldName,
|
|
458
|
-
|
|
459
|
-
if (def.filterable !== true)
|
|
458
|
+
for (const [fieldName, def] of Object.entries(entity.fields)) {
|
|
459
|
+
if (!("filterable" in def) || def.filterable !== true)
|
|
460
460
|
continue;
|
|
461
461
|
if (def.type === "boolean") {
|
|
462
462
|
for (const key of booleanFacetOptionKeys(feature.name, screen.entity, fieldName)) {
|
|
@@ -513,7 +513,7 @@ export function buildEffectiveTranslationKeys(features) {
|
|
|
513
513
|
for (const key of Object.keys(feature.translations ?? {})) {
|
|
514
514
|
out.add(`${feature.name}:${key}`);
|
|
515
515
|
// Blank form too — Settings-Hub group namespaces (`group: "tenant-settings"`)
|
|
516
|
-
// require `${group}.settings` which is not `${feature}:${key}
|
|
516
|
+
// require `${group}.settings` which is not `${feature}:${key}`.
|
|
517
517
|
out.add(key);
|
|
518
518
|
}
|
|
519
519
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createDbConnection } from "../db/connection.ts";
|
|
2
|
-
export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.ts";
|
|
2
|
+
export { acquireSessionAdvisoryLock, createDatabase, databaseExists, dropDatabaseIfExists, listDatabaseNamesWithPrefix, renameDatabase, truncateTablesRestartIdentity, } from "../db/queries/test-stack.ts";
|
|
3
3
|
export { type AnyDb, isClosedConnectionError } from "../db/query.ts";
|
|
4
4
|
export { columnNamesOf } from "../db/schema-inspection.ts";
|
|
5
5
|
export { schemaTableName } from "../db/table-introspection.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @runtime dev
|
|
2
2
|
export { createDbConnection } from "../db/connection.js";
|
|
3
|
-
export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
|
|
3
|
+
export { acquireSessionAdvisoryLock, createDatabase, databaseExists, dropDatabaseIfExists, listDatabaseNamesWithPrefix, renameDatabase, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
|
|
4
4
|
export { isClosedConnectionError } from "../db/query.js";
|
|
5
5
|
export { columnNamesOf } from "../db/schema-inspection.js";
|
|
6
6
|
export { schemaTableName } from "../db/table-introspection.js";
|
|
@@ -3,7 +3,7 @@ import type { ConfigAccessor } from "@cosmicdrift/kumiko-types/config";
|
|
|
3
3
|
import type { TzContext } from "@cosmicdrift/kumiko-types/tz-context";
|
|
4
4
|
import type { JobsOptions, Queue } from "bullmq";
|
|
5
5
|
import type { TenantDb } from "../db/tenant-db.ts";
|
|
6
|
-
import type { AppContext,
|
|
6
|
+
import type { AppContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
|
|
7
7
|
import type { Logger } from "../logging/types.ts";
|
|
8
8
|
import type { Meter, SerializedTraceContext, Tracer } from "../observability/index.ts";
|
|
9
9
|
import type { JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
|
|
@@ -14,7 +14,6 @@ export declare function serializeJobSubject(subject: Readonly<Record<string, Job
|
|
|
14
14
|
export declare function jobSubjectKey(jobName: string, payload: Record<string, unknown>, fields: readonly string[] | undefined, declaration: "tenantVisibleFailure" | "tenantVisibleRun"): string | null;
|
|
15
15
|
export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">): Promise<TzContext>;
|
|
16
16
|
export declare function missingDbProxy(jobName: string): TenantDb;
|
|
17
|
-
export declare function createCrossTenantReads(systemModeDb: TenantDb, reason: string, report: EscapeHatchReporter): NonNullable<JobContext["crossTenantReads"]>;
|
|
18
17
|
export declare function priorAttemptsOf(data: Record<string, unknown>): number;
|
|
19
18
|
export declare function tenantFailureMessageKey(err: unknown, declaration: JobDefinition["tenantVisibleFailure"]): string | null;
|
|
20
19
|
export declare const TRACE_CONTEXT_KEY = "_traceContext";
|
|
@@ -66,6 +66,12 @@ export function serializeJobSubject(subject) {
|
|
|
66
66
|
.sort()
|
|
67
67
|
.map((field) => [field, subject[field] ?? null]));
|
|
68
68
|
}
|
|
69
|
+
function isJobSubjectValue(value) {
|
|
70
|
+
return (value === null ||
|
|
71
|
+
typeof value === "string" ||
|
|
72
|
+
typeof value === "number" ||
|
|
73
|
+
typeof value === "boolean");
|
|
74
|
+
}
|
|
69
75
|
// Stable identity for the declared payload fields: sorted keys, so two runs
|
|
70
76
|
// with the same subject values produce the same string and the later one
|
|
71
77
|
// replaces the earlier record. Non-primitives would serialize into something
|
|
@@ -76,7 +82,7 @@ export function jobSubjectKey(jobName, payload, fields, declaration) {
|
|
|
76
82
|
const subject = {};
|
|
77
83
|
for (const field of fields) {
|
|
78
84
|
const value = payload[field] ?? null;
|
|
79
|
-
if (value
|
|
85
|
+
if (!isJobSubjectValue(value)) {
|
|
80
86
|
throw new Error(`Job "${jobName}": ${declaration}.subjectFields["${field}"] must be a primitive, got ${typeof value}`);
|
|
81
87
|
}
|
|
82
88
|
subject[field] = value;
|
|
@@ -103,22 +109,6 @@ export function missingDbProxy(jobName) {
|
|
|
103
109
|
},
|
|
104
110
|
});
|
|
105
111
|
}
|
|
106
|
-
export function createCrossTenantReads(systemModeDb, reason, report) {
|
|
107
|
-
return {
|
|
108
|
-
selectMany(table, where, options) {
|
|
109
|
-
report("cross-tenant-read", reason);
|
|
110
|
-
return systemModeDb.selectMany(table, where, options);
|
|
111
|
-
},
|
|
112
|
-
fetchOne(table, where) {
|
|
113
|
-
report("cross-tenant-read", reason);
|
|
114
|
-
return systemModeDb.fetchOne(table, where);
|
|
115
|
-
},
|
|
116
|
-
count(table, where) {
|
|
117
|
-
report("cross-tenant-read", reason);
|
|
118
|
-
return systemModeDb.count(table, where);
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
112
|
export function priorAttemptsOf(data) {
|
|
123
113
|
const value = data["_priorAttempts"];
|
|
124
114
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
|
|
@@ -266,7 +256,7 @@ export function payloadMatchesTriggerFilter(where, payload) {
|
|
|
266
256
|
// on the children still existing in Redis when a wrapper retry re-derives
|
|
267
257
|
// their ids. The margin here is generous — queue wait time isn't bounded by
|
|
268
258
|
// backoff — to cover realistic windows. Audit trail lives in read_job_runs,
|
|
269
|
-
// not BullMQ
|
|
259
|
+
// not BullMQ.
|
|
270
260
|
export const COMPLETED_JOB_RETENTION_AGE_SEC = 86_400; // 24h
|
|
271
261
|
export const FAILED_JOB_RETENTION_AGE_SEC = 604_800; // 7d
|
|
272
262
|
// Total wall-clock time BullMQ can hold a perTenant wrapper across all its
|
|
@@ -7,7 +7,7 @@ export declare const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-
|
|
|
7
7
|
* BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
|
|
8
8
|
* scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
|
|
9
9
|
* treated as old repeatables — spawning a new scheduler entry every tick and
|
|
10
|
-
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
10
|
+
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
11
11
|
* publicstatus Redis OOM). Strip `.` and `:` from the job QN.
|
|
12
12
|
*/
|
|
13
13
|
export declare function schedulerIdForJobName(jobName: string): string;
|
|
@@ -19,13 +19,13 @@ export const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-ids";
|
|
|
19
19
|
* BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
|
|
20
20
|
* scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
|
|
21
21
|
* treated as old repeatables — spawning a new scheduler entry every tick and
|
|
22
|
-
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
22
|
+
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
23
23
|
* publicstatus Redis OOM). Strip `.` and `:` from the job QN.
|
|
24
24
|
*/
|
|
25
25
|
export function schedulerIdForJobName(jobName) {
|
|
26
26
|
return `scheduler-${jobName.replace(/[.:]/g, "-")}`;
|
|
27
27
|
}
|
|
28
|
-
// Same colon-in-BullMQ-id hazard as schedulerIdForJobName
|
|
28
|
+
// Same colon-in-BullMQ-id hazard as schedulerIdForJobName —
|
|
29
29
|
// a QN like "publicstatus:job:uptime-probe" must not leave ":" in the id.
|
|
30
30
|
export function bootJobIdForJobName(jobName) {
|
|
31
31
|
return `boot-${jobName.replace(/[.:]/g, "-")}`;
|
|
@@ -97,8 +97,7 @@ export async function removeOrphanJobSchedulers(queue, expectedSchedulerIds, lan
|
|
|
97
97
|
// duplicates — safe only as long as the child job hash outlives the
|
|
98
98
|
// wrapper's retry window, enforced at construction against
|
|
99
99
|
// COMPLETED_JOB_RETENTION_AGE_SEC (see the invariant check in
|
|
100
|
-
// createJobRunner).
|
|
101
|
-
// (fw#1603/#1604) — the wrapper id already contains ":", so strip
|
|
100
|
+
// createJobRunner). The wrapper id already contains ":", so strip
|
|
102
101
|
// separators from both halves before joining instead of interpolating raw.
|
|
103
102
|
export function perTenantChildJobId(wrapperJobId, tenantId) {
|
|
104
103
|
const sanitize = (value) => value.replace(/[.:]/g, "-");
|