@cosmicdrift/kumiko-framework 0.363.0 → 0.364.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/AGENTS.md +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +7 -0
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +46 -17
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +8 -3
- package/dist/bun-db/query.js +27 -7
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/eagerload.js +2 -2
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +24 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.js +9 -8
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +2 -0
- package/dist/db/tenant-db.js +30 -4
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -8
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +1 -1
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +8 -4
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +99 -22
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +36 -9
- package/package.json +7 -5
- package/src/changes.json +207 -8
package/src/changes.json
CHANGED
|
@@ -1,4 +1,203 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "A throwing command in a batch without a database connection releases its cap reservation",
|
|
6
|
+
"detail": "The sequential fallback of dispatchBatch (no DB connection) did not release the reserved capacity when a command threw instead of returning a failure result. It now releases the reservation before rethrowing."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.364.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Write handlers can declare a beforeTransaction phase, and file bytes can be uploaded before the transaction",
|
|
12
|
+
"detail": "defineWriteHandler accepts beforeTransaction(event, ctx), which the dispatcher runs after the pre-handler gates and any reserveBeforeTransaction and before the transaction opens, so a slow external call holds no pool connection. Its data reaches the handler as event.prepared; a failure result or throw rejects the write, and its release runs whenever the transaction does not commit. ctx.write, appendEvent, notify and the other write surfaces throw inside the phase, and a handler with the phase cannot run as a nested ctx.write. uploadFileBytesBeforeTransaction writes the bytes in that phase and createFileRefFromUploadedBytes creates the fileRef row in the handler; the release keeps the bytes when the row exists. Original files whose row never committed stay in storage, no job sweeps them."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.364.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "Boot-validator and guard messages name the fix",
|
|
18
|
+
"detail": "Boot-validator and guard messages that only described the problem now also say what to do about it. The German configEdit and retention messages are in English. Validation and guard behavior is unchanged."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.364.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "CacheSyncBus.publish takes { remoteOnly } to skip same-process listeners",
|
|
24
|
+
"detail": "`publish(topic, message, { remoteOnly: true })` delivers only to other processes. Use it when the publisher has already applied the change locally and a local fanout would trigger a redundant reload."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.364.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "changes.json no longer ships paths into private repositories",
|
|
30
|
+
"detail": "Older changelog entries named file paths and a docs path of private repositories. They now name only the repository. The changelog guard rejects a `<private-repo>/<path>` reference or a github.com link into a private repository."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.364.0",
|
|
34
|
+
"type": "fix",
|
|
35
|
+
"title": "coerceRow no longer claims the raw driver row already has the converted shape",
|
|
36
|
+
"detail": "coerceRow now accepts a Record<string, unknown> driver row and returns the explicitly requested row type, with the single cast at that boundary. The raw event-consumer, pending-event and projection-rebuild queries return untyped rows instead of a caller-chosen generic. The eventMigrations JSDoc and the expand/contract guide now state that pii and encrypted fields reach transforms as field-bound ciphertext and must not be renamed or derived from."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"version": "0.364.0",
|
|
40
|
+
"type": "fix",
|
|
41
|
+
"title": "collectTableMetas rejects two features registering the same table name",
|
|
42
|
+
"detail": "Two entities or storeTables with the same table name produced duplicate metas that only failed later in `schema generate` with a message about a hand-added meta. collectTableMetas now throws at the first registration with both origins, so setupTestStack fails the same way. The duplicate-name error of the migration generator no longer assumes a hand-added meta."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"version": "0.364.0",
|
|
46
|
+
"type": "fix",
|
|
47
|
+
"title": "System-cron escape hatch audits are deduplicated per target and repeat daily",
|
|
48
|
+
"detail": "The per-process dedup of declared system-cron hatches keyed only on handler, kind and tenant, so a run writing several global tables was audited for the first table only, and a worker running longer than the audit retention left no event. The key now includes the target and the process audits again after 24 hours."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"version": "0.364.0",
|
|
52
|
+
"type": "improvement",
|
|
53
|
+
"title": "A detail() cache hit with an ownership predicate costs one database statement",
|
|
54
|
+
"detail": "The ownership re-check and the live stream-version read for a cached detail row now run as a single query, so a cache hit is no more expensive than a cache miss for entities with SQL ownership rules."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"version": "0.364.0",
|
|
58
|
+
"type": "fix",
|
|
59
|
+
"title": "A later cookieDomain or secureCookies:false no longer leaves stale __Host- auth and csrf cookies behind",
|
|
60
|
+
"detail": "expireLegacySessionCookieVariants covers both directions. With unprefixed names and secure cookies, setAuthCookies and clearSessionCookies now also expire __Host-kumiko_auth and __Host-kumiko_csrf as Secure, Path=/, host-only. Before, the browser kept sending the stale __Host-kumiko_csrf in X-CSRF-Token and every write failed with csrf_token_mismatch until it expired."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"version": "0.364.0",
|
|
64
|
+
"type": "fix",
|
|
65
|
+
"title": "Expired fileRef purge no longer stalls on rows that keep failing",
|
|
66
|
+
"detail": "The soft-delete cleanup always selected the oldest 500 expired fileRefs, so 500 permanently failing rows (unsafe key, missing list permission) blocked every newer expired fileRef of that tenant. A run now walks the expired rows page by page along (deletedAt, id) and ends on an empty page; failed rows are retried on the next run."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"version": "0.364.0",
|
|
70
|
+
"type": "fix",
|
|
71
|
+
"title": "Extra routes outside /api get the request body cap for every entry",
|
|
72
|
+
"detail": "extraRoutes with entry anonymous or user outside /api now run behind the maxRequestBytes body limit like signature routes already did, so an unauthenticated caller can no longer stream an oversized body into a public form or callback route."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"version": "0.364.0",
|
|
76
|
+
"type": "fix",
|
|
77
|
+
"title": "FileContext.refFromFileRef reads files whose fileRef moved to another tenant",
|
|
78
|
+
"detail": "A tenant handover moves the fileRef row but not the bytes, so the storageKey keeps the previous tenant's prefix and the tenant-bound `ctx.files.ref(key)` rejected it. FileContext gains `refFromFileRef({ storageKey })` for keys read from a fileRef row the caller has already tenant-checked: it skips the tenant-prefix check and still rejects unsafe keys. The variant route and ctx.derivatives.variant use it, so variants of handed-over files render again. `ref(key)` keeps the prefix check for freely supplied keys."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"version": "0.364.0",
|
|
82
|
+
"type": "improvement",
|
|
83
|
+
"title": "A save with file fields loads its fileRef rows once",
|
|
84
|
+
"detail": "The tenant check and the pending-upload binding of a save each read every named fileRef row inside the write transaction. They now share one query over the ids, restricted to the writer's tenant and live rows. Values that are not UUIDs are still skipped."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"version": "0.364.0",
|
|
88
|
+
"type": "improvement",
|
|
89
|
+
"title": "Agent guide and generated indexes ship in the framework package",
|
|
90
|
+
"detail": "The framework package now contains AGENTS.md plus an agents/ directory with generated feature, widget and example indexes, built from feature-manifest.json, the renderer-web exports and samples/recipes. The new agent-docs-drift guard in kumiko check fails when the committed files differ from the generator output. New apps get an AGENTS.md and a CLAUDE.md that point at the packaged guide."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"version": "0.364.0",
|
|
94
|
+
"type": "breaking",
|
|
95
|
+
"title": "Handler field references support business keys and array fields",
|
|
96
|
+
"migration": "handlerFieldReference(schema, field) now returns { entity, by, multiple } instead of the entity string. Replace reference with reference.entity; skip or handle fields where by is set (value is a business key, not an id) or multiple is true (value is an array)."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"version": "0.364.0",
|
|
100
|
+
"type": "fix",
|
|
101
|
+
"title": "An empty jsonText match list no longer returns every row",
|
|
102
|
+
"detail": "A where filter `{ jsonText: [] }` on a jsonb column used to add no condition, so the query returned all rows. It now matches no rows, like `in: []`. A `null` or non-object entry in the list throws the descriptive jsonText error instead of a bare TypeError."
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"version": "0.364.0",
|
|
106
|
+
"type": "improvement",
|
|
107
|
+
"title": "Facet field docs name the reference exception",
|
|
108
|
+
"detail": "The docs of projectionList.facets and relatedList.facets said every facet field must be a declared column. A facet of type \"reference\" is exempt: it filters by id through the bound query's filters and needs a registered entity instead. Behavior is unchanged."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"version": "0.364.0",
|
|
112
|
+
"type": "fix",
|
|
113
|
+
"title": "list() reads rows and stream versions in one statement",
|
|
114
|
+
"detail": "The entity list read the rows first and the stream versions in a second statement, so a concurrent write could pair old row data with a newer version, and an update built on that version would pass the optimistic lock. The stream version is now a correlated subquery of the list statement, the same way detail() reads it."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"version": "0.364.0",
|
|
118
|
+
"type": "fix",
|
|
119
|
+
"title": "Existing tenant indexes pick up searchable fields added to the registry default",
|
|
120
|
+
"detail": "The lazy default configuration now also applies to an index whose stored searchable, filterable and sortable attributes are a strict subset of the current default (an older default after a deploy added fields). Indexes holding any attribute outside the current default are treated as an explicit configure() and stay untouched, as before."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"version": "0.364.0",
|
|
124
|
+
"type": "breaking",
|
|
125
|
+
"title": "Writes without an open transaction run as their own command; ctx.db writes there throw",
|
|
126
|
+
"detail": "Write handlers accept transaction \"perCommand\". Such a handler runs without a transaction of its own, so every ctx.write and ctx.writeAs it issues runs as its own top-level command with its own transaction, gates and afterCommit hooks. A perCommand handler cannot run as a nested ctx.write, cannot be part of a multi-command batch (422 per_command_handler_in_batch) and cannot combine with beforeTransaction or reserveBeforeTransaction (boot error). ctx.write and ctx.writeAs from stream handlers and top-level query handlers now take the same route, so afterCommit hooks of the called write, which never fired before, fire now. ctx.db writes (entity executor, insertOne, updateMany, deleteMany, db.global() writes) and event appends (appendEvent, unsafeAppendEvent, tryAppendEvent, fetchForWriting, archiveStream, restoreStream, snapshotAggregate) throw in stream handlers, top-level query handlers, perCommand handlers and in beforeTransaction and reserveBeforeTransaction. ctx.dbOutsideTransaction and ctx.outsideTransaction stay available, and ctx.db.unsafeRaw stays open as the audited escape hatch.",
|
|
127
|
+
"migration": "Stream and query code that writes through ctx.db or an executor bound to ctx.db calls a write handler through ctx.write or ctx.writeAs instead, or writes deliberately through ctx.dbOutsideTransaction. Check code that relied on afterCommit hooks of a write called from a stream or query handler not firing. bookCapUsage from a stream or query handler needs outsideTransaction true or a command. runInOwnTransaction(ctx.db, ...) stays locked in these contexts; runInOwnTransaction(ctx.dbOutsideTransaction, ...) is the way to a deliberate own transaction. Measured consumers: one stream handler (AI agent turn: conversation, activity and tool-call records through an executor on ctx.db, plus cap booking) and one query handler (AI text run: cap booking on ctx.db, log via ctx.write). No other consumer app has stream or query writes. The beforeTransaction/reserveBeforeTransaction lock affects no released code."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"version": "0.364.0",
|
|
131
|
+
"type": "fix",
|
|
132
|
+
"title": "decryptForRead and eager-loaded refs decrypt file, image, files and images fields that hold legacy ciphertext",
|
|
133
|
+
"detail": "Only the encrypt side skips file-reference fields. The decrypt side uses collectPiiSubjectFields again and passes plaintext through unchanged, so rows written by the old backfill, which encrypted the file uuid, are read as plaintext instead of failing with pii_ciphertext_leak."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"version": "0.364.0",
|
|
137
|
+
"type": "fix",
|
|
138
|
+
"title": "Cascade deletes honour the record-lock exemptions and ignore soft-deleted children",
|
|
139
|
+
"detail": "The cascade hook blocked a parent delete on a locked child even when the executor delete had skipped the lock guard (unsafeSkipLockGuard, a framework system write with a writePurpose, forget). DeleteContext now carries lockGuardBypassed and the hook follows it. Soft-deleted children no longer count as locked."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"version": "0.364.0",
|
|
143
|
+
"type": "fix",
|
|
144
|
+
"title": "reserveBeforeTransaction contexts now reject writes that would escape the transaction",
|
|
145
|
+
"detail": "The context passed to reserveBeforeTransaction now throws on ctx.write, writeAs, event appends, scheduleAfterCommit, notify and jobRunner, like the beforeTransaction phase does. dbOutsideTransaction, outsideTransaction and forTenant stay available because reservations deliberately write outside the transaction."
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"version": "0.364.0",
|
|
149
|
+
"type": "fix",
|
|
150
|
+
"title": "Restore no longer writes plaintext PII from pre-KMS rows into the event log",
|
|
151
|
+
"detail": "Restore now decrypts the soft-deleted row and re-encrypts it like delete before storing previous, so rows written before the subject KMS was configured no longer leak plaintext personal fields into the restored event. Restoring such a row for an erased subject now fails like delete."
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"version": "0.364.0",
|
|
155
|
+
"type": "fix",
|
|
156
|
+
"title": "bindReferencedFiles binds only refs of the record's tenant in one bulk load",
|
|
157
|
+
"detail": "File refs for a page are loaded with a single query restricted to the writer's tenant and live rows; system-owned refs are not bound to records."
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"version": "0.364.0",
|
|
161
|
+
"type": "fix",
|
|
162
|
+
"title": "Boot validator covers option, prefill, emptyState, anonymous rate-limit and searchable-access gaps",
|
|
163
|
+
"detail": "Rejects optionsQuery combined with optionsAvailabilityQuery, invalid dashboard valueFormat kinds, from-today panels without ranges or brush, params on emptyState navigate/drawer, unknown submitPrefilled keys, per-user rate limits on tenantless anonymous handlers, unknown conditional select option values, and searchable fields (including embedded sub-fields) the reader cannot access."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"version": "0.364.0",
|
|
167
|
+
"type": "fix",
|
|
168
|
+
"title": "Malformed requestId or batch command shapes return 400 instead of failing later",
|
|
169
|
+
"detail": "The dispatch and batch routes parse requestId and each command before dispatching, returning a validation error for malformed shapes, and the JSON body reader no longer relies on a type assertion."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"version": "0.364.0",
|
|
173
|
+
"type": "fix",
|
|
174
|
+
"title": "files/images maxCount is validated on write",
|
|
175
|
+
"detail": "The generated write schema applies .max(maxCount) to files and images fields, so payloads with more ids than allowed are rejected with a validation error."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"version": "0.364.0",
|
|
179
|
+
"type": "fix",
|
|
180
|
+
"title": "render/parse roundtrip keeps secrets valid when writeRoles is empty",
|
|
181
|
+
"detail": "renderSecret and renderSecretNamespace omit writeRoles when the list is empty so the rendered source parses back to the same definition."
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"version": "0.364.0",
|
|
185
|
+
"type": "fix",
|
|
186
|
+
"title": "A stale auth cookie on the SSE stream ends the web session with a session-ended frame",
|
|
187
|
+
"detail": "With anonymousAccess, a stale or revoked auth cookie downgrades /api/sse to an anonymous 200, so the web client never noticed the session end and live updates stopped silently. The stream now opens with a session-ended frame carrying the rejection code and expires the auth cookies; the renderer-web live-events client calls onSessionEnded once for it."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"version": "0.364.0",
|
|
191
|
+
"type": "fix",
|
|
192
|
+
"title": "Unit tests no longer leak a console.warn spy into later files",
|
|
193
|
+
"detail": "The search payload precedence tests spy on console.warn per test and restore it, so later files in the same --no-isolate worker start with the real console.warn. The mail editor missing-language test waits for the placeholder rail instead of reading it before the validate query settled."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"version": "0.364.0",
|
|
197
|
+
"type": "fix",
|
|
198
|
+
"title": "Calling transaction() on a tx or savepoint handle fails with an InternalError",
|
|
199
|
+
"detail": "`RawClient.begin` is now typed optional, and `transaction()` checks it like `runInNewTransaction` does, so a nested call raises an InternalError (\"runner has no begin()\") instead of a TypeError. The migrate runner requires `begin` on its client and rejects handles without it before running any SQL."
|
|
200
|
+
},
|
|
2
201
|
{
|
|
3
202
|
"version": "0.363.0",
|
|
4
203
|
"type": "improvement",
|
|
@@ -123,7 +322,7 @@
|
|
|
123
322
|
"type": "breaking",
|
|
124
323
|
"title": "Unbound uploads are private to the uploader (security)",
|
|
125
324
|
"detail": "The default file access guard denies every operation on a file with no entityType or no entityId unless the caller is the uploader or has a role in privilegedRoles; the file routes answer 404 as for any denied file. Before, files without entityType were readable by the whole tenant and pending uploads until a record claimed them. bindPendingFileRefs now also runs for updates and binds the file ids named in the update's changes for file, image, files and images fields (same entityType and fieldName, entityId null, insertedById = caller), so createFileRefFromBytes with attachTo but no entityId plus a following update write attaches the file to the record.",
|
|
126
|
-
"migration": "Code that shows a file without entityType to other users (tenant logo, avatar, mail attachment, import preview, AI-agent output) must upload it with entityType and fieldName of a file field on a record and save the record, so readers of that record can read it; or keep it uploader-only, run the read as a role in privilegedRoles, or set a custom accessGuard in the files feature options. Apps that set accessGuard keep their own decision. Code that reads another user's pending upload before the record is saved must bind the file first or run as the uploader. Known case: solon's document upload
|
|
325
|
+
"migration": "Code that shows a file without entityType to other users (tenant logo, avatar, mail attachment, import preview, AI-agent output) must upload it with entityType and fieldName of a file field on a record and save the record, so readers of that record can read it; or keep it uploader-only, run the read as a role in privilegedRoles, or set a custom accessGuard in the files feature options. Apps that set accessGuard keep their own decision. Code that reads another user's pending upload before the record is saved must bind the file first or run as the uploader. Known case: solon's document upload posts without entityType and the later document record (created by the ingest job, not the uploader) stores the id in its `fileId` field, so other users no longer read the original; solon must upload with entityType and fieldName of `fileId` and bind the file when the record is created, because a different creator cannot claim the pending upload."
|
|
127
326
|
},
|
|
128
327
|
{
|
|
129
328
|
"version": "0.361.0",
|
|
@@ -156,7 +355,7 @@
|
|
|
156
355
|
"type": "breaking",
|
|
157
356
|
"title": "Schema generate rejects a table listed twice in ENTITY_METAS",
|
|
158
357
|
"detail": "When ENTITY_METAS contained the same tableName twice, the migration diff kept only the last definition and compared it against the previous snapshot entry, so a new index was written to .snapshot.json but no CREATE INDEX reached the migration SQL. kumiko-schema generate, kumiko-schema validate and programmatic generateMigration now throw (generate and validate exit 1 with a message naming every duplicated table) before any file is written.",
|
|
159
|
-
"migration": "Remove hand-added metas for tables a feature already registers from kumiko/schema.ts. Known: phronexsis
|
|
358
|
+
"migration": "Remove hand-added metas for tables a feature already registers from kumiko/schema.ts. Known: phronexsis's schema file appends IN_APP_MESSAGES_META next to collectTableMetas(FEATURES); drop it (and its comment), then run kumiko-schema generate — the existing in_app_messages_tenant_user_created_idx comes out as CREATE INDEX IF NOT EXISTS (no-op) and the two legacy indexes as commented review drops. The duplicate in_app_messages entry in .snapshot.json disappears with that generate."
|
|
160
359
|
},
|
|
161
360
|
{
|
|
162
361
|
"version": "0.361.0",
|
|
@@ -182,7 +381,7 @@
|
|
|
182
381
|
"type": "breaking",
|
|
183
382
|
"title": "Secure host-only session cookies use the __Host- prefix",
|
|
184
383
|
"detail": "Auth, csrf and tenant cookies are named __Host-kumiko_auth, __Host-kumiko_csrf and __Host-kumiko_tenant when they are written Secure and without a Domain (the tenant cookie is always host-only). With auth.cookieDomain or without Secure the auth and csrf names stay kumiko_auth and kumiko_csrf. New auth option secureCookies forces Secure (default NODE_ENV === \"production\"). Login deletes the legacy unprefixed host-only cookies, logout and deleteTenantCookie delete the prefixed and the legacy names. The server reads only the resolved names, so a request that carries only the legacy kumiko_auth is anonymous. AUTH_COOKIE_NAME, CSRF_COOKIE_NAME and TENANT_COOKIE_NAME are removed from the framework and dispatcher-live; the base names and sessionCookieName() are exported from @cosmicdrift/kumiko-types/session-cookies and @cosmicdrift/kumiko-framework/api, getSessionCookies(c) returns the resolved names of a request. The testing package's CSRF_COOKIE_NAME stays and resolves to the unprefixed name (e2e servers run without Secure).",
|
|
185
|
-
"migration": "Every user logs in once after the deploy, because the old kumiko_auth cookie is no longer read. Custom clients, proxies or scripts that read or forward the cookies by literal name break: read the names through sessionCookieName() / resolveSessionCookieNames() (types package) or getSessionCookies(c) on the server, and handle both the prefixed and the plain name in browser code (readCsrfToken() already does). Replace imports of AUTH_COOKIE_NAME, CSRF_COOKIE_NAME and TENANT_COOKIE_NAME with AUTH_COOKIE_BASE, CSRF_COOKIE_BASE and TENANT_COOKIE_BASE plus sessionCookieName(); e2e helpers that imported CSRF_COOKIE_NAME from @cosmicdrift/kumiko-dispatcher-live import it from @cosmicdrift/kumiko-testing/e2e instead (known: solon e2e
|
|
384
|
+
"migration": "Every user logs in once after the deploy, because the old kumiko_auth cookie is no longer read. Custom clients, proxies or scripts that read or forward the cookies by literal name break: read the names through sessionCookieName() / resolveSessionCookieNames() (types package) or getSessionCookies(c) on the server, and handle both the prefixed and the plain name in browser code (readCsrfToken() already does). Replace imports of AUTH_COOKIE_NAME, CSRF_COOKIE_NAME and TENANT_COOKIE_NAME with AUTH_COOKIE_BASE, CSRF_COOKIE_BASE and TENANT_COOKIE_BASE plus sessionCookieName(); e2e helpers that imported CSRF_COOKIE_NAME from @cosmicdrift/kumiko-dispatcher-live import it from @cosmicdrift/kumiko-testing/e2e instead (known: solon's e2e helpers and specs, and a kumiko-enterprise integration test that uses AUTH_COOKIE_NAME). A reverse proxy that strips or rewrites cookie names must pass the __Host- names unchanged. Staging or preview deployments over plain http need no change (Secure is off there); set auth.secureCookies (also accepted by runProdApp) explicitly when TLS terminates before the app and NODE_ENV is not \"production\". A standalone createAuthRoutes mount outside buildServer must also mount sessionCookiesMiddleware with the same resolved names, otherwise the readers fall back to the domain-less default."
|
|
186
385
|
},
|
|
187
386
|
{
|
|
188
387
|
"version": "0.360.0",
|
|
@@ -254,7 +453,7 @@
|
|
|
254
453
|
"type": "breaking",
|
|
255
454
|
"title": "Entity PII fields need whenAbsent: \"tenant\" when the owner field is nullable",
|
|
256
455
|
"detail": "personal: { of: \"<owner>\", whenAbsent: \"tenant\" } makes rows without an owner editable, deletable and forgettable with an active KMS, without plaintext PII. The subject is resolved in this order: the owner on the previous row, the owner the same event sets, then the tenant key (tenant:<tenantId>). Before, an update, delete or forget of an ownerless row failed with SubjectResolutionError (HTTP 500) once a KMS was configured. backfill-pii follows the same chain and checks the projection row for the owner before it falls back to the tenant key; an updated event whose previous.owner is null but whose changes.owner is set no longer loses the owner. Entity fields have no plaintext fallback, so \"tenant\" is the only value. bundled jobs: cron and system runs (triggeredById null) now store payload, error and log messages as ciphertext under the system tenant key instead of plaintext.",
|
|
257
|
-
"migration": "Boot now aborts for a personal: { of: \"<owner>\" } field whose owner field is not required and that declares no whenAbsent: [Feature X] Field \"f\" on entity \"e\" is personal: { of: \"ownerId\" }, but \"ownerId\" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: \"ownerId\", whenAbsent: \"tenant\" } } to encrypt those rows under the tenant key, or make \"ownerId\" required: true. Entity fields have no plaintext fallback (fw#3654). Fix: add whenAbsent: \"tenant\", or make the owner field required: true if every row has one. whenAbsent on personal: { of: \"id\" } throws at declaration. Known
|
|
456
|
+
"migration": "Boot now aborts for a personal: { of: \"<owner>\" } field whose owner field is not required and that declares no whenAbsent: [Feature X] Field \"f\" on entity \"e\" is personal: { of: \"ownerId\" }, but \"ownerId\" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: \"ownerId\", whenAbsent: \"tenant\" } } to encrypt those rows under the tenant key, or make \"ownerId\" required: true. Entity fields have no plaintext fallback (fw#3654). Fix: add whenAbsent: \"tenant\", or make the owner field required: true if every row has one. whenAbsent on personal: { of: \"id\" } throws at declaration. Known consumers: kumiko-enterprise (entities with an ownerUserId, authorId or ownerId owner field) and offlot-app (a lead entity with enquiryId). Existing plaintext job-run rows of cron runs stay readable; new ones are ciphertext."
|
|
258
457
|
},
|
|
259
458
|
{
|
|
260
459
|
"version": "0.357.0",
|
|
@@ -336,7 +535,7 @@
|
|
|
336
535
|
"version": "0.354.0",
|
|
337
536
|
"type": "breaking",
|
|
338
537
|
"title": "enrichWithReferences and enrichRowWithReferences require a viewer { user, parentVisibility }",
|
|
339
|
-
"migration": "Custom handlers pass `{ user: query.user, parentVisibility: { entities: ctx.registry.getAllEntities() } }` as the fifth argument. No direct callers are known in the workspace apps. Apps can now point reference fields at team- or row-scoped entities (for example solon unitId
|
|
538
|
+
"migration": "Custom handlers pass `{ user: query.user, parentVisibility: { entities: ctx.registry.getAllEntities() } }` as the fifth argument. No direct callers are known in the workspace apps. Apps can now point reference fields at team- or row-scoped entities (for example solon's unitId and propertyId)."
|
|
340
539
|
},
|
|
341
540
|
{
|
|
342
541
|
"version": "0.354.0",
|
|
@@ -658,7 +857,7 @@
|
|
|
658
857
|
"type": "breaking",
|
|
659
858
|
"title": "Key Manager slots are required, LEGACY_SLOTS default removed",
|
|
660
859
|
"detail": "Key Manager slots are required: no more LEGACY_SLOTS default\n`resolvePlatformKeks`, `resolveKmsWiringAsync` and `requireKmsWiringAsync` no longer fall back to the three platform slots; `slots` is a required option. `runSchemaCli` called with `features` now requires `kmsSlots` and exits 1 with a hint before any DB connect or migration apply when it is missing. `kmsSlots: []` stays valid for apps that wire no Key Manager slots. The scaffolded `bin/kumiko.ts` passes `kmsSlots` derived from the composed env schema.",
|
|
661
|
-
"migration": "Pass `slots` (for example `kmsSlotsOf(<app>ComposedEnv.schema)`) to `resolvePlatformKeks`, `resolveKmsWiringAsync` and `requireKmsWiringAsync`. In `bin/kumiko.ts`, `runSchemaCli(..., { features })` needs `kmsSlots: kmsSlotsOf(<app>ComposedEnv.schema)` (or `[]` when the app wires no Key Manager slots. With `[]` every `*_CIPHERTEXT` env var is ignored, so an app that ships a ciphertext must pass its slots); without it the CLI exits 1 before applying migrations.\nWorkspace state at release: show-pony `bin/kumiko.ts` and its
|
|
860
|
+
"migration": "Pass `slots` (for example `kmsSlotsOf(<app>ComposedEnv.schema)`) to `resolvePlatformKeks`, `resolveKmsWiringAsync` and `requireKmsWiringAsync`. In `bin/kumiko.ts`, `runSchemaCli(..., { features })` needs `kmsSlots: kmsSlotsOf(<app>ComposedEnv.schema)` (or `[]` when the app wires no Key Manager slots. With `[]` every `*_CIPHERTEXT` env var is ignored, so an app that ships a ciphertext must pass its slots); without it the CLI exits 1 before applying migrations.\nWorkspace state at release: show-pony `bin/kumiko.ts` and its copy in the kumiko-platform docs samples still call `runSchemaCli` with `features` only. publicstatus, solon, offlot-app, money-horse, phronexsis and kumiko-studio already pass `kmsSlots` / `slots`."
|
|
662
861
|
},
|
|
663
862
|
{
|
|
664
863
|
"version": "0.342.0",
|
|
@@ -2065,7 +2264,7 @@
|
|
|
2065
2264
|
"type": "breaking",
|
|
2066
2265
|
"title": "TenantDataHookCtx/UserDataHookCtx.db is a tenant-filtered TenantDb; unfiltered hook access needs r.useExtension({ escapeHatch }) (fw#2914 Zugang 3).",
|
|
2067
2266
|
"detail": "EXT_TENANT_DATA `destroy` hooks (`TenantDataHookCtx.db`) and EXT_USER_DATA `export`/`delete` hooks (`UserDataHookCtx.db`) no longer receive the unfiltered `DbRunner` that was bound to the extension's owning handler. Both are now a `TenantDb` in \"tenant\" mode, built via the public `createTenantDb(...)`, bound to the hook's `tenantId`: reads/writes go through `ctx.db.selectMany/fetchOne/insertOne/updateMany/deleteMany`, already scoped to that tenant plus `SYSTEM_TENANT_ID` reference rows. `r.useExtension(EXT_USER_DATA | EXT_TENANT_DATA, entityName, { ... })` gains an `escapeHatch?: { reason: string }` option, validated non-empty at registration time (throws at boot for an empty/whitespace reason). Declaring it grants `ctx.db.unsafeRaw(reason)` inside that entity's hooks — same reason string — which returns the real, unfiltered `DbRunner`; without it, `ctx.db.unsafeRaw(reason)` throws `AccessDeniedError`. Every granted use reports an `\"unsafe-raw\"` escape-hatch audit event (`{ handler: \"userData:<entity>\"|\"tenantData:<entity>\", kind, reason, tenantId, actor }`) through the caller's `_escapeHatchAuditSink` when wired, else a `security:escape-hatch-used` warn-log — same #2861 pattern PR #2934 already applied to the job runner, and the same one fw#2914's earlier JobContext.db and read-step changes already applied elsewhere in the framework. `run-forget-cleanup.ts`/`run-user-export.ts` (user-data-rights) thread `escapeHatchAuditSink`/`actor` from their owning `r.job`'s `ctx._escapeHatchAuditSink`/`ctx.systemUser.id` down to each hook's `TenantDb`, so a declared use is attributed to the real job run, not `\"<unattributed>\"`.\n`asRawClient(ctx.db)` inside a hook now throws immediately: `asRawClient` rejects a `TenantDb`-shaped argument (\"received a tenant-scoped TenantDb (ctx.db). Raw SQL helpers do not apply the tenant filter — use a ctx.db.<method>, or ctx.db.unsafeRaw(reason) with escapeHatch: { reason } ...\"). The framework's `DbRunner`-typed free functions (`selectMany(db, table, where)`, `fetchOne`, `insertOne`, `updateMany`, `deleteMany`, `createTenantDb(db, tenantId, mode)`, etc. from `@cosmicdrift/kumiko-framework/bun-db`/`/db`) now reject `ctx.db` as their first argument at compile time too — `TenantDb` does not structurally satisfy `DbRunner` (no `.unsafe`/`.begin`).\nA table built via `createEntity`+`buildEntityTable`+`r.entity(...)` carries the pre-existing `EXECUTOR_ONLY` brand, so `ctx.db.insertOne/updateMany/deleteMany(table, ...)` against it is a compile error by design (\"a managed projection is only writable through the executor\"); a hook that needs to write such a table directly (bulk hard-delete, jsonb key removal, an UPDATE expression `updateMany`'s literal-value `set` can't express) needs the `escapeHatch` + `ctx.db.unsafeRaw(reason)` route, filtering by tenant/owner in the SQL text itself — the escape hatch removes automatic tenant-filtering, not the hook author's responsibility to filter. A table built via `defineUnmanagedTable` carries no such brand and stays writable through `ctx.db`'s typed methods directly.\nThis PR's own `custom-fields` user-data-rights wiring (`wire-user-data-rights.ts`) uses the escape hatch for exactly this reason: `selectCustomFieldsHostRows` runs against a dynamic host table name unknown at compile time, so it can't use `ctx.db`'s typed methods. CI's `AST-Guards (shared runner)` step (`@cosmicdriftgamestudio/kumiko-guards@^0.36.0`, pinned) does not yet recognize this call shape — `isEscapeHatchDeclaredFunction` in `guard-escape-hatch-declared.ts` only matches a property/method literally named `handler`, or a direct call to `hook`/`writeHandler`/`queryHandler`/`streamHandler`; `r.useExtension(EXT_USER_DATA, entityName, { export: exportHook, escapeHatch })` matches neither, because `useExtension` isn't in `ESCAPE_HATCH_CALL_METHODS` and the hook property here is `export`/`delete`, never `handler`. This also isn't fixable by baselining from kumiko-framework: `security-baselines/<repo>.json` ships inside the pinned `@cosmicdriftgamestudio/kumiko-guards` npm package itself, and `run-guards.ts`'s own comment states it is rewritten only by \"the infra baseline-maintenance run over all repos on main\" — never by a consuming repo's local run, and never persisted through a consuming repo's `bun install`. The fix belongs in the guard's owning repo (presumably `infra`, per this workspace's root `CLAUDE.md`): add `useExtension` to `ESCAPE_HATCH_CALL_METHODS`, or otherwise recognize the framework's actual hook-property names (`export`/`delete`/`destroy`), then release a new `@cosmicdriftgamestudio/kumiko-guards` version and bump the range in kumiko-framework's `package.json`. Until that lands, this PR's `AST-Guards (shared runner)` CI check stays red on this one finding; every other check is green.",
|
|
2068
|
-
"migration": "A hook that only reads/writes its own tenant through `ctx.db` method calls (`ctx.db.selectMany/fetchOne/insertOne/updateMany/deleteMany`) needs no change — it now gets the same tenant filter every other `TenantDb` consumer already had. A hook that treated `ctx.db` as a raw connection (`asRawClient(ctx.db)`, `ctx.db` passed as the first argument to a free-function `selectMany`/`fetchOne`/`insertOne`/`updateMany`/`deleteMany`/`createTenantDb`, a cast to `DbRunner`/`DbConnection`, or `\"raw\" in db ? db.raw : db`) must add `escapeHatch: { reason: \"<why>\" }` to its `r.useExtension(EXT_USER_DATA | EXT_TENANT_DATA, entityName, { ... })` registration and fetch the runner explicitly with `ctx.db.unsafeRaw(\"<why>\")` (same reason string) at the point of use. Prefer converting to `ctx.db`'s typed methods first — reach for `unsafeRaw` only when the target table is `EXECUTOR_ONLY`-branded (a managed `r.entity(...)` table; typed writes reject it by design — see the `EXECUTOR_ONLY`/`WritableTable` note in Detail) or the SQL needs an expression the typed API can't produce (e.g. a jsonb `-` key-removal `UPDATE`). No codemod — cross-hook raw-SQL usage is expected to be rare enough, and tenant-filter-relevant enough, to review by hand per hook. Hand-built `TenantDataHookCtx`/`UserDataHookCtx` objects in tests need `db: createTenantDb(conn, tenantId, \"tenant\", undefined, undefined, undefined, { unsafeRaw: { reason: \"<why>\" } })` instead of a plain connection.\n\nAudit at release
|
|
2267
|
+
"migration": "A hook that only reads/writes its own tenant through `ctx.db` method calls (`ctx.db.selectMany/fetchOne/insertOne/updateMany/deleteMany`) needs no change — it now gets the same tenant filter every other `TenantDb` consumer already had. A hook that treated `ctx.db` as a raw connection (`asRawClient(ctx.db)`, `ctx.db` passed as the first argument to a free-function `selectMany`/`fetchOne`/`insertOne`/`updateMany`/`deleteMany`/`createTenantDb`, a cast to `DbRunner`/`DbConnection`, or `\"raw\" in db ? db.raw : db`) must add `escapeHatch: { reason: \"<why>\" }` to its `r.useExtension(EXT_USER_DATA | EXT_TENANT_DATA, entityName, { ... })` registration and fetch the runner explicitly with `ctx.db.unsafeRaw(\"<why>\")` (same reason string) at the point of use. Prefer converting to `ctx.db`'s typed methods first — reach for `unsafeRaw` only when the target table is `EXECUTOR_ONLY`-branded (a managed `r.entity(...)` table; typed writes reject it by design — see the `EXECUTOR_ONLY`/`WritableTable` note in Detail) or the SQL needs an expression the typed API can't produce (e.g. a jsonb `-` key-removal `UPDATE`). No codemod — cross-hook raw-SQL usage is expected to be rare enough, and tenant-filter-relevant enough, to review by hand per hook. Hand-built `TenantDataHookCtx`/`UserDataHookCtx` objects in tests need `db: createTenantDb(conn, tenantId, \"tenant\", undefined, undefined, undefined, { unsafeRaw: { reason: \"<why>\" } })` instead of a plain connection.\n\nAudit at release: kumiko-enterprise has seven user-data hook files across five packages that take `ctx.db` as a raw `DbRunner` argument (`createTenantDb(ctx.db, ctx.tenantId, \"system\")` and free-function `selectMany(ctx.db, table, where)`). Each needs `escapeHatch` on its `r.useExtension(...)` plus `createTenantDb(ctx.db.unsafeRaw(\"<why>\"), ctx.tenantId, \"system\")`, and the reads become `ctx.db.selectMany(table, where)` (already tenant-scoped, no \"system\" mode needed). They must be migrated in kumiko-enterprise itself before it bumps its pinned `@cosmicdrift/kumiko-framework`/`@cosmicdrift/kumiko-types` version, or `tsc` fails.\n\nGuard-version gap: a consumer that declares `escapeHatch` on a `r.useExtension(...)` registration needs a `@cosmicdriftgamestudio/kumiko-guards` release newer than `^0.36.0`; the shared AST guard run of older versions does not recognize it. Bump the guards range before or alongside this migration, not after."
|
|
2069
2268
|
},
|
|
2070
2269
|
{
|
|
2071
2270
|
"version": "0.279.0",
|
|
@@ -2196,7 +2395,7 @@
|
|
|
2196
2395
|
"type": "fix",
|
|
2197
2396
|
"title": "Projection/MSP rebuild aborts instead of silently dropping row level security from the live table (fw#2907).",
|
|
2198
2397
|
"detail": "New `assertLiveTableHasNoRowLevelSecurity(tx, tableName, projectionName?)` in `db/queries/shadow-swap.ts` checks `pg_class.relrowsecurity`/`relforcerowsecurity` and `pg_policy` (public schema) for the live table before the shadow swap. `rebuildProjection` and `rebuildMultiStreamProjection` now call it before `buildShadowTable`, alongside the existing `assertLiveColumnsMatchMeta` fence, and `swapShadowIntoLive` re-checks it again right before the cutover under the fence. Previously the shadow, built from `EntityTableMeta` (which carries no RLS/policy information), would swap over a live table with RLS enabled/forced or any policy and silently drop all of it. `replayMigrationsDir`'s shape-neutral ALTER-clause recognizer (`db/replay-migration-sql.ts`) now also accepts `FORCE`/`NO FORCE ROW LEVEL SECURITY` instead of throwing \"unparsed table-DDL statement\".",
|
|
2199
|
-
"migration": "No action for tables without RLS. A table with RLS enabled/forced or any policy can no longer be rebuilt online — rebuild fails with the table name; drop the RLS/policies or keep the table out of rebuild
|
|
2398
|
+
"migration": "No action for tables without RLS. A table with RLS enabled/forced or any policy can no longer be rebuilt online — rebuild fails with the table name; drop the RLS/policies or keep the table out of rebuild."
|
|
2200
2399
|
},
|
|
2201
2400
|
{
|
|
2202
2401
|
"version": "0.271.0",
|