@cosmicdrift/kumiko-framework 0.364.0 → 0.365.1
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 +36 -5
- package/agents/examples/anonymous-access-multitenant.md +9 -6
- package/agents/examples/anonymous-access.md +14 -26
- package/agents/examples/auth-mfa.md +5 -9
- package/agents/examples/custom-fields-basic.md +7 -6
- package/agents/examples/custom-handlers.md +72 -25
- package/agents/examples/dashboard-stats.md +229 -0
- package/agents/examples/encrypted-tenant-config.md +17 -8
- package/agents/examples/error-contract.md +1 -5
- package/agents/examples/feature-toggles.md +28 -17
- package/agents/examples/ledger-banking.md +7 -6
- package/agents/examples/ledger-invoicing.md +7 -6
- package/agents/examples/legal-pages.md +2 -2
- package/agents/examples/managed-pages.md +2 -2
- package/agents/examples/notes-history-basic.md +7 -6
- package/agents/examples/ownership.md +30 -22
- package/agents/examples/pdf-audit-export.md +87 -51
- package/agents/examples/session-revocation.md +1 -10
- package/agents/examples/tags-basic.md +7 -7
- package/agents/examples/user-data-rights.md +25 -17
- package/agents/examples/webhook-step.md +11 -1
- package/agents/examples/workflow-engine.md +2 -2
- package/agents/examples.md +2 -1
- package/agents/features.md +4 -4
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +44 -43
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/server.d.ts +2 -2
- package/dist/api/server.js +29 -4
- package/dist/bun-db/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +7 -0
- package/dist/bun-db/query.js +50 -18
- package/dist/crypto/in-memory-kms-adapter.js +4 -7
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pg-kms-adapter.js +10 -6
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +2 -1
- package/dist/db/event-store-executor-context.d.ts +1 -0
- package/dist/db/event-store-executor-context.js +1 -0
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +85 -58
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.js +1 -0
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +56 -31
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- package/dist/db/tenant-db.d.ts +2 -0
- package/dist/db/tenant-db.js +20 -1
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -1
- package/dist/engine/boot-validator/entity-handler.js +3 -0
- package/dist/engine/boot-validator/query-refs.js +14 -2
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +21 -0
- package/dist/engine/registry.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/steps/_read-source.js +5 -2
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/files/create-file-ref.d.ts +2 -1
- package/dist/files/create-file-ref.js +16 -2
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.js +4 -0
- package/dist/files/file-routes.js +9 -5
- package/dist/files/index.d.ts +1 -1
- package/dist/files/index.js +1 -1
- package/dist/files/purge-expired-file-ref-bytes.js +2 -3
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/job-runner.d.ts +2 -0
- package/dist/jobs/job-runner.js +183 -48
- package/dist/jobs/max-per-tenant.d.ts +8 -0
- package/dist/jobs/max-per-tenant.js +14 -0
- package/dist/observability/escape-hatch-report.d.ts +1 -0
- package/dist/observability/escape-hatch-report.js +31 -2
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +1 -1
- package/dist/pipeline/dispatch-shared.js +41 -30
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/multi-stream-apply-context.d.ts +1 -0
- package/dist/pipeline/multi-stream-apply-context.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +2 -1
- package/dist/schema-cli.js +12 -1
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +4 -4
- package/src/changes.json +323 -0
package/dist/upgrade-cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { getFlag, getStringFlag, parseArgs } from "./arg-parser.js";
|
|
5
5
|
import { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries, } from "./engine/index.js";
|
|
6
6
|
import { ensureTemporalPolyfill } from "./time/index.js";
|
|
7
7
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8
|
+
const SEMVER_CORE_RE = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?$/;
|
|
8
9
|
const CODEMOD_SUBDIR = "scripts/codemod";
|
|
9
10
|
function readPackageJsonVersion(packageJsonPath) {
|
|
10
11
|
if (!existsSync(packageJsonPath))
|
|
@@ -21,20 +22,49 @@ function readPackageJsonVersion(packageJsonPath) {
|
|
|
21
22
|
}
|
|
22
23
|
// bun.lock pins transitive packages too, so a repo that only depends on
|
|
23
24
|
// kumiko-cli/-guards still records the bundled-features version it resolved.
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// Prerelease and snapshot pins (0.364.0-next.1, 0.0.0-pr-123) are reduced to
|
|
26
|
+
// their x.y.z core, the only shape the rest of the CLI compares.
|
|
27
|
+
function readLockfileVersion(dir, pkgName) {
|
|
28
|
+
const lockPath = join(dir, "bun.lock");
|
|
26
29
|
if (!existsSync(lockPath))
|
|
27
30
|
return null;
|
|
28
|
-
const pinned = new RegExp(`"@cosmicdrift/${pkgName}": \\["@cosmicdrift/${pkgName}@(
|
|
29
|
-
return pinned?.[1] ?? null;
|
|
31
|
+
const pinned = new RegExp(`"@cosmicdrift/${pkgName}": \\["@cosmicdrift/${pkgName}@([^"]+)"`).exec(readFileSync(lockPath, "utf-8"));
|
|
32
|
+
return pinned?.[1] === undefined ? null : (SEMVER_CORE_RE.exec(pinned[1])?.[1] ?? null);
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
function declaresWorkspaces(packageJsonPath) {
|
|
35
|
+
if (!existsSync(packageJsonPath))
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
39
|
+
return typeof pkg === "object" && pkg !== null && "workspaces" in pkg;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isRepoBoundary(dir) {
|
|
46
|
+
return existsSync(join(dir, ".git")) || declaresWorkspaces(join(dir, "package.json"));
|
|
47
|
+
}
|
|
48
|
+
function readPackageVersionAt(dir, pkgName, repoLocalPath) {
|
|
49
|
+
return (readPackageJsonVersion(join(dir, `node_modules/@cosmicdrift/${pkgName}/package.json`)) ??
|
|
50
|
+
readPackageJsonVersion(join(dir, repoLocalPath)) ??
|
|
51
|
+
readLockfileVersion(dir, pkgName));
|
|
52
|
+
}
|
|
53
|
+
// Walks up from a workspace member to the repo's own root (.git or a
|
|
54
|
+
// package.json with workspaces) but never past it: walking on from a worktree
|
|
55
|
+
// nested in a parent workspace read that workspace's version, and the
|
|
56
|
+
// isolated linker keeps transitive packages out of node_modules.
|
|
34
57
|
function readPackageVersion(root, pkgName, repoLocalPath) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
let dir = root;
|
|
59
|
+
for (;;) {
|
|
60
|
+
const version = readPackageVersionAt(dir, pkgName, repoLocalPath);
|
|
61
|
+
if (version !== null || isRepoBoundary(dir))
|
|
62
|
+
return version;
|
|
63
|
+
const parent = dirname(dir);
|
|
64
|
+
if (parent === dir)
|
|
65
|
+
return null;
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
38
68
|
}
|
|
39
69
|
// Changelog entries come from @cosmicdrift/kumiko-bundled-features (see
|
|
40
70
|
// findFeaturesDirs); comparing against the framework version instead
|
|
@@ -508,6 +538,8 @@ markerVersion, previous) {
|
|
|
508
538
|
function parseResolveInput(args) {
|
|
509
539
|
if (getFlag(args, "apply"))
|
|
510
540
|
return { error: "--resolve cannot be combined with --apply." };
|
|
541
|
+
if (getFlag(args, "dry-run"))
|
|
542
|
+
return { error: "--resolve cannot be combined with --dry-run." };
|
|
511
543
|
const refs = (getStringFlag(args, "resolve") ?? "")
|
|
512
544
|
.split(",")
|
|
513
545
|
.map((ref) => ref.trim())
|
|
@@ -565,8 +597,9 @@ function resolveManualMigrations(out, args, targetDir) {
|
|
|
565
597
|
if (errors.length > 0)
|
|
566
598
|
return fail(...errors, "Nothing was written.");
|
|
567
599
|
const { reason } = input;
|
|
568
|
-
const
|
|
569
|
-
|
|
600
|
+
const resolution = getFlag(args, "not-applicable")
|
|
601
|
+
? "not-applicable"
|
|
602
|
+
: "migrated";
|
|
570
603
|
const resolvedAt = Temporal.Now.instant().toString();
|
|
571
604
|
const newlyResolved = open
|
|
572
605
|
.filter((entry) => resolvedIds.has(entry.id))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.1",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -210,8 +210,8 @@
|
|
|
210
210
|
"./package.json": "./package.json"
|
|
211
211
|
},
|
|
212
212
|
"dependencies": {
|
|
213
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
214
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
213
|
+
"@cosmicdrift/kumiko-http": "0.365.1",
|
|
214
|
+
"@cosmicdrift/kumiko-types": "0.365.1",
|
|
215
215
|
"bullmq": "^5.76.7",
|
|
216
216
|
"bun-types": "^1.3.13",
|
|
217
217
|
"hono": "^4.13.1",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
"zod": "^4.4.3"
|
|
228
228
|
},
|
|
229
229
|
"devDependencies": {
|
|
230
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
230
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.365.1",
|
|
231
231
|
"bun-types": "^1.3.13",
|
|
232
232
|
"pino-pretty": "^13.1.3"
|
|
233
233
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,327 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.1",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "App and worker boot wait for Postgres instead of crashing",
|
|
6
|
+
"detail": "runProdApp and worker processes now probe the database before the schema-drift check and retry with exponential backoff (1s to 15s, up to 90s) on transient connection errors such as too_many_connections or ECONNREFUSED. The KMS health check at boot retries the same way. Non-transient errors like auth failures still abort immediately."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.1",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Restoring a soft-deleted row with a money field no longer fails with a 500",
|
|
12
|
+
"detail": "restore() wrote the raw stored row (money as bigint minor units) into payload.previous, and the jsonb bind threw. It now builds previous in the same domain form as delete(), so the restored event carries { amount, currency, ... } for money fields."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.365.1",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "version_conflict from delete, restore, forget and update reports the actual stream version",
|
|
18
|
+
"detail": "After a lost append race, delete, restore and forget returned currentVersion -1 and update returned the version read before the race, so expectedVersion and currentVersion looked equal. They now look up the stream version after the conflict; -1 remains only when that lookup fails."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.365.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "ctx.forTenant is available read-only in the beforeTransaction phase",
|
|
24
|
+
"detail": "A beforeTransaction phase can now resolve ctx.forTenant(tenantId) for its own tenant and, as a SystemAdmin, for a foreign tenant, to read rows or build tenant-scoped inputs such as uploads. The returned db denies every write; a foreign tenant stays gated by the SystemAdmin check. reserveBeforeTransaction, perCommand and in-transaction handlers are unchanged."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.365.0",
|
|
28
|
+
"type": "breaking",
|
|
29
|
+
"title": "Boot checks from the review round 1 release reject configurations that used to boot",
|
|
30
|
+
"detail": "These checks shipped as fixes without migration notes. Each one throws for a configuration that was accepted before:\n- a where-rule subquery on a table whose columns cannot be derived\n- a drawer prefill that targets a sensitive field or a field the form does not render\n- two secret fields with the same id\n- access.personalData \"public-intake\" combined with rateLimit: { disabled: true }\n- a recordOwned field on an entity with idType \"serial\"\n- dedupeFeatures for same-named features with different registrations\nThe agent-tools manifest check in buildToolCatalog throws when the catalog is built for a request, not at boot.",
|
|
31
|
+
"migration": "Boot every app against the new version before rolling it out, and send one agent tool request if the app mounts agent-tools. Give where-rule subqueries a table with derivable columns, prefill only rendered non-sensitive fields, make secret field ids unique, give public-intake handlers a rateLimit that is not disabled, switch recordOwned entities to idType \"uuid\" or change the field's personal stance, and rename or align duplicated features."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.365.0",
|
|
35
|
+
"type": "breaking",
|
|
36
|
+
"title": "Boot rejects visibleWhen queries with required input, non-sql index where clauses, lockedWhen on read-restricted fields and same-named features with a different surface",
|
|
37
|
+
"detail": "These checks used to be absent or silent and now stop the app at boot:\n- A screen or dashboard panel visibleWhen that names a query whose input schema does not accept an empty payload. The client gate runs it with an empty payload, so the screen stayed hidden.\n- An entity index where that is not a sql expression. It used to be dropped, which built a full (unique) index.\n- A lockedWhen field that declares access.read.\n- Two features with the same name whose screens, navs, jobs, events, translations, config keys, notifications, metrics or hooks differ. The second one used to be dropped.",
|
|
38
|
+
"migration": "Boot each app once against the new version. For a visibleWhen query, make its input fields optional or point visibleWhen at an input-free query. Write index where clauses with sql`...`. Remove access.read from the lockedWhen field or lock on a field every role can read. Give differing same-named features distinct names or make their surfaces identical."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"version": "0.365.0",
|
|
42
|
+
"type": "fix",
|
|
43
|
+
"title": "Bun.SQL provider parity fixes for DB_PROVIDER=bun",
|
|
44
|
+
"detail": "Array params bind as Postgres array literals, transaction handles are detected correctly, Instant params are stored at millisecond precision, integer params reject fractions, and migrations hold their advisory lock on one pinned connection. LISTEN/NOTIFY uses Bun.SQL listen() on its own connection, which the pool idle timeout does not close, so the Bun provider no longer opens a postgres-js peer."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"version": "0.365.0",
|
|
48
|
+
"type": "fix",
|
|
49
|
+
"title": "cardColumnRoles skips hideOnNarrow images",
|
|
50
|
+
"detail": "The first image column that is not hideOnNarrow becomes the card preview; narrow-hidden image columns neither become the preview nor the title."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"version": "0.365.0",
|
|
54
|
+
"type": "fix",
|
|
55
|
+
"title": "Console-spy tests only count the messages of the code under test",
|
|
56
|
+
"detail": "Unit tests that asserted a silent console.warn/error ran in a process shared with other test files and picked up unrelated async warnings. They now filter the spied calls to the messages the tested code produces. No runtime change."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"version": "0.365.0",
|
|
60
|
+
"type": "improvement",
|
|
61
|
+
"title": "AGENTS.md covers the gaps from the agent-docs eval; new dashboard-stats recipe",
|
|
62
|
+
"detail": "AGENTS.md now explains personalData \"tenant-members\" for openToAll writes, personal { of: \"id\" } for records that are the person, counting with list filter and totalCount instead of filtering one page, the dashboard stat panels, that schema:generate needs a name and that a newly mounted bundled feature needs a migration, and that r.crud needs write.access and read.access. The new dashboard-stats recipe shows a dashboard with a stat-group and a stat backed by one counting query. No runtime change."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"version": "0.365.0",
|
|
66
|
+
"type": "fix",
|
|
67
|
+
"title": "Same-named features that differ in screens, navs, jobs, events or hooks no longer merge silently",
|
|
68
|
+
"detail": "dedupeFeatures now compares the keys of screens, navs, jobs, events, translations, config keys, notifications, metrics and hooks in addition to entities and handlers, so a second same-named feature with different surface throws instead of being dropped."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"version": "0.365.0",
|
|
72
|
+
"type": "fix",
|
|
73
|
+
"title": "Moved file ids are not disposed",
|
|
74
|
+
"detail": "disposeRemovedFileRefs now collects the file ids still stored in any file field of the saved record before soft-deleting removed ones, so an id moved from one field to another stays readable."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"version": "0.365.0",
|
|
78
|
+
"type": "fix",
|
|
79
|
+
"title": "A .docx upload must contain a word/ entry, not just the bytes \"word/\"",
|
|
80
|
+
"detail": "validateFileContent now reads the ZIP central directory and requires an entry name that starts with word/. ZIPs with entries such as password/a.txt or xl/word/sheet.xml, or the bytes appearing anywhere in the file, are rejected as content_mismatch."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"version": "0.365.0",
|
|
84
|
+
"type": "improvement",
|
|
85
|
+
"title": "zipWithEntries test helper in @cosmicdrift/kumiko-framework/testing",
|
|
86
|
+
"detail": "Builds a minimal ZIP with a real central directory for upload tests. Hand-made docx fixtures that only had a local header and the word/ bytes no longer pass the content check; build them with zipWithEntries(\"word/document.xml\") instead."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"version": "0.365.0",
|
|
90
|
+
"type": "fix",
|
|
91
|
+
"title": "Executor update and delete with writePurpose erasure, and forget, write the erased sentinel for subjects whose key is gone",
|
|
92
|
+
"detail": "Re-encrypting a pre-KMS plaintext row after eraseKey threw subject-erased, so the erase flows could not finish their own follow-up writes. Update and delete with writePurpose erasure, and forget always, now replace the PII fields of an erased subject in changes and previous with the sentinel; fields of other subjects in the same row stay encrypted. Without writePurpose and for restore the subject-erased conflict stays. The forget-subject door close and the forget cleanup pass writePurpose erasure to the user lifecycle update."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"version": "0.365.0",
|
|
96
|
+
"type": "improvement",
|
|
97
|
+
"title": "Error docsUrl falls back to the framework page of the error code for uncovered app reasons",
|
|
98
|
+
"detail": "An error whose details.reason is an app reason not covered by errorDocs, such as the not_found reason of every NotFoundError, now carries docsUrl pointing at the framework page of its error code (for example /errors/not_found) instead of no docsUrl. Reasons covered by errorDocs keep the app link; framework reasons are unchanged."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"version": "0.365.0",
|
|
102
|
+
"type": "fix",
|
|
103
|
+
"title": "Escape-hatch audits are collected for the whole stream",
|
|
104
|
+
"detail": "Dispatched streams run inside the escape-hatch audit scope for the full lifetime of the stream, and settled pending audits are dropped from the scope, so writes made while a stream is open are no longer missed."
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"version": "0.365.0",
|
|
108
|
+
"type": "fix",
|
|
109
|
+
"title": "Soft-delete cleanup and hardDelete retention delete the bytes of handed-over files",
|
|
110
|
+
"detail": "A tenant handover moves a fileRef row to the new tenant, but its storageKey keeps the previous tenant's prefix. The soft-delete cleanup and the data-retention hardDelete went through `ctx.files.ref()` and `ctx.files.list()`, which reject keys outside the job's tenant, so the bytes and derivatives of such files were never deleted and the soft-delete cleanup kept the row forever. Both now delete through the new `deleteFileRefBytes(files, row, context)` from `@cosmicdrift/kumiko-framework/derivatives`, built on the new `FileContext.listFromFileRef(row)` (the row's key plus its derivatives, under the same trust as `refFromFileRef`). Form-draft discard deletes draft files through `refFromFileRef` as well."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"version": "0.365.0",
|
|
114
|
+
"type": "fix",
|
|
115
|
+
"title": "Removing a file that was moved to another file field of the same record deletes its fileRef",
|
|
116
|
+
"detail": "The fileRef stayed bound to the field it was first uploaded to. Once it was moved to a second field and then removed from that field, the dispose step skipped it and the file stayed readable. Dispose now accepts a binding to any file field of the same record."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"version": "0.365.0",
|
|
120
|
+
"type": "fix",
|
|
121
|
+
"title": "MFA verify without a switch-tenant gate fails at boot, and the gate result carries the session roles",
|
|
122
|
+
"detail": "createAuthRoutes throws when mfaVerifyHandler is set without switchTenantMfaGateHandler, and runDevApp and runProdApp check that the gate handler is registered before wiring, so a missing gate can no longer let switch-tenant skip MFA. The gate handler returns the roles with its clear result and the route mints the target session from them. assertRetiredCookieDomains is no longer exported, and the auth-session signup variant is composed from SignupOutcome. Setups that set mfaVerifyHandler must also register the switch-tenant gate handler."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"version": "0.365.0",
|
|
126
|
+
"type": "fix",
|
|
127
|
+
"title": "fetchForWriting reads one consistent stream version, nested ctx.write calls run in order, ctx.notify works through ctx.query",
|
|
128
|
+
"detail": "fetchForWriting derives the stream version from the same events it loads instead of a second read. Parallel ctx.write calls on one transaction are serialized, so a failing write rolls back only its own savepoint and no longer takes a concurrent successful write with it. ctx.notify called inside a handler reached through ctx.query is deferred onto the outer write's after-commit list instead of being delivered before that write commits. ctx.notify snapshots its options when it is called, so later mutation by the handler does not change the delivered notification. The JSDoc of AggregateStreamHandle.appendOne now states that it must not be mixed with other appends to the same aggregate in one handler."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"version": "0.365.0",
|
|
132
|
+
"type": "fix",
|
|
133
|
+
"title": "registerEntityCrud merges per-verb agent hints into write.agent and read.agent",
|
|
134
|
+
"detail": "agents[verb] used to replace write.agent or read.agent wholesale, so write: { agent: { expose: true } } with agents: { delete: { risk: \"high\" } } dropped expose on delete. The two are now merged with the per-verb value winning. An escapeHatch update/delete/restore on a row whose tenantId is not a valid TenantId throws an InternalError instead of silently appending to the operator's own tenant stream."
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"version": "0.365.0",
|
|
138
|
+
"type": "breaking",
|
|
139
|
+
"title": "Entity list and detail handlers with escapeHatch must declare SystemAdmin-only access",
|
|
140
|
+
"detail": "defineEntityListHandler, defineEntityDetailHandler and registerEntityCrud read options with escapeHatch now throw at definition unless access is { roles: [\"SystemAdmin\"] }, like write handlers already did. A cross-tenant read reachable by any tenant role leaked foreign rows.",
|
|
141
|
+
"migration": "Give every list/detail handler that declares escapeHatch access: { roles: [\"SystemAdmin\"] }, or drop the escapeHatch if the handler should stay tenant-scoped."
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"version": "0.365.0",
|
|
145
|
+
"type": "fix",
|
|
146
|
+
"title": "Upload route creates the fileRef row in its own transaction, cleans up bytes on failure and excludes system files from unclaimedUploadReaders",
|
|
147
|
+
"detail": "The file routes and the API server use createProjectingTenantDb so projections on fileRef.created run. When the row insert or a projection throws, the upload route rolls the row back, deletes the stored bytes and answers 500. Files uploaded by the system user are no longer readable through unclaimedUploadReaders."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"version": "0.365.0",
|
|
151
|
+
"type": "fix",
|
|
152
|
+
"title": "Cron schedulers of a running job runner survive orphan cleanup by an older runner, and stop() waits for a timed-out boot gate",
|
|
153
|
+
"detail": "The runner re-stamps its own cron schedulers in the orphan-cleanup seen hash every quarter of the grace period (one hset for all ids, cleared in stop()), so a mixed-version window longer than an hour no longer lets an older runner delete a scheduler a live runner holds. The stamps are written before the schedulers are upserted. A boot gate that exceeds its timeout is abandoned, not aborted: its handler keeps running, and stop() now waits for it (bounded) before closing connections."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"version": "0.365.0",
|
|
157
|
+
"type": "fix",
|
|
158
|
+
"title": "buildServer throws at boot when a handler declares additionalRateLimits but no RateLimitResolver can be built",
|
|
159
|
+
"detail": "Without a Redis client or context.rateLimit every call to such a handler (auth-email-password signup and token requests, waitlist submit) answered 500 at request time. buildServer now throws at boot and names the handlers.",
|
|
160
|
+
"migration": "Wire a Redis client on context.redis (or pass context.rateLimit). For the waitlist without Redis, pass emailRateLimits: []."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"version": "0.365.0",
|
|
164
|
+
"type": "fix",
|
|
165
|
+
"title": "PII backfill keeps ownerless events under the tenant key and re-keys values left under a previous owner",
|
|
166
|
+
"detail": "With resolveOwnerFromProjection the projection owner is now used only when the event does not carry the owner key at all; an explicit null owner resolves through whenAbsent like the live write path. The owner re-key stage (rekeyTenantKeyedOwnerFields) also moves user-keyed values to the projection's current owner when that is another user, so after an owner change X to Y that predates re-keying on transition, forgetting Y erases the value. Known divergence: a row that went ownerless, then owned by X, then ownerless again keeps its created-event value under the tenant key, while the live path keeps it with X."
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"version": "0.365.0",
|
|
170
|
+
"type": "improvement",
|
|
171
|
+
"title": "pruneEvents batchSize and _notifyAfterCommitFactory on the handler context",
|
|
172
|
+
"detail": "pruneEvents takes an optional batchSize that deletes the lowest event ids first, so callers can loop until deletedCount is below the batch size with one short transaction per batch. The handler context exposes _notifyAfterCommitFactory, a notify factory bound to the dispatcher's job runner for use after the transaction has committed."
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"version": "0.365.0",
|
|
176
|
+
"type": "improvement",
|
|
177
|
+
"title": "requireTierResolverPlugin narrows a tier-resolver registration or fails the boot",
|
|
178
|
+
"detail": "requireTierResolverPlugin(usage) returns the TierResolverPlugin of a tenantTierResolver registration and throws \"tier-resolver registration options must provide a build() factory\" otherwise. runProdApp, the worker boot and runDevApp share it, so the boot error is identical in all three entry points."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"version": "0.365.0",
|
|
182
|
+
"type": "improvement",
|
|
183
|
+
"title": "Back-dated seed writes fail with a typed SeedTimeOrderError",
|
|
184
|
+
"detail": "append() throws the exported SeedTimeOrderError (aggregateType, aggregateId, version, seedCreatedAt, predecessorCreatedAt) when a back-dated event would precede its stream predecessor, so seeders can tell it from other failures without parsing the message. The check now runs inside the insert transaction, so a concurrent live append can no longer land between check and insert."
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"version": "0.365.0",
|
|
188
|
+
"type": "fix",
|
|
189
|
+
"title": "kumiko-upgrade --resolve rejects --dry-run, and the installed version is found from workspace members and prerelease lockfile pins",
|
|
190
|
+
"detail": "kumiko-upgrade --resolve --dry-run used to write the marker anyway; it now exits with an error and writes nothing. The installed version is now looked up from the working directory up to the repo root (the first directory with .git or a package.json with workspaces), so running inside a workspace member of a hoisted-linker monorepo finds it, while a parent workspace outside the repo is still ignored. bun.lock pins like 0.364.0-next.1 resolve to their x.y.z core instead of being dropped."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"version": "0.365.0",
|
|
194
|
+
"type": "fix",
|
|
195
|
+
"title": "Non-sql index where throws instead of building a full index",
|
|
196
|
+
"detail": "buildEntityTable no longer drops a malformed EntityIndexDef.where silently, which produced a full (unique) index that blocked reuse of soft-deleted values. moneyAmount is typed as a bigint column."
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"version": "0.365.0",
|
|
200
|
+
"type": "improvement",
|
|
201
|
+
"title": "Job handlers get ctx.metricsFor like request handlers",
|
|
202
|
+
"detail": "JobContext now exposes ctx.metricsFor(featureName) with the same contract as HandlerContext, so shared code can count under one stable feature name from jobs too. Unregistered names stay a silent no-op."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"version": "0.365.0",
|
|
206
|
+
"type": "fix",
|
|
207
|
+
"title": "A failed scheduler seen stamp at start no longer lets an older runner remove fresh cron schedulers for good",
|
|
208
|
+
"detail": "When the initial seen stamp failed, a concurrent older runner could delete the new runner's cron schedulers as orphans, and the periodic refresh only re-stamped them. The refresh now stamps and upserts the lane's cron schedulers again until both succeeded once."
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"version": "0.365.0",
|
|
212
|
+
"type": "fix",
|
|
213
|
+
"title": "Job runner start survives a failed scheduler-seen stamp",
|
|
214
|
+
"detail": "The initial scheduler-seen stamp is best-effort like the periodic refresh and the orphan cleanup: a Redis error is logged as a warning and the runner still starts and registers its cron schedulers."
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"version": "0.365.0",
|
|
218
|
+
"type": "fix",
|
|
219
|
+
"title": "lockedWhen on a field with access.read fails at boot",
|
|
220
|
+
"detail": "Roles that cannot read the lock field receive records without it, and the client then showed every record as locked. validateLockedWhen now rejects a lock field that declares access.read."
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"version": "0.365.0",
|
|
224
|
+
"type": "breaking",
|
|
225
|
+
"title": "moneyAmount returns ColumnBuilder<bigint> instead of ColumnBuilder<number>",
|
|
226
|
+
"detail": "The column was always created with bigint mode, so the driver returned a bigint while the type said number. The type now matches the runtime value. Code that assigned the column value to a number or did number arithmetic on it no longer typechecks.",
|
|
227
|
+
"migration": "Treat values read from a moneyAmount column as bigint (for example compare with 0n, or convert explicitly with Number() where the amount is known to fit). No schema change."
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"version": "0.365.0",
|
|
231
|
+
"type": "improvement",
|
|
232
|
+
"title": "The pdf-audit-export recipe uploads the PDF before the transaction",
|
|
233
|
+
"detail": "The shipped pdf-audit-export example now renders and uploads the PDF in a beforeTransaction phase with uploadFileBytesBeforeTransaction and creates the fileRef row in the handler with createFileRefFromUploadedBytes, so the upload holds no pool connection. No runtime change."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"version": "0.365.0",
|
|
237
|
+
"type": "fix",
|
|
238
|
+
"title": "The pii-personal-migration codemod skips .d.ts files in migration mode too",
|
|
239
|
+
"detail": "An earlier release note said the codemod now skips .d.ts files in --report-stance mode. That mode already skipped them. What changed is that migration mode skips .d.ts files as well, matching --report-stance."
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"version": "0.365.0",
|
|
243
|
+
"type": "fix",
|
|
244
|
+
"title": "No false narrowing warning for where.tenantId given as { in } or { eq }",
|
|
245
|
+
"detail": "The read.findOne and read.findMany steps unwrap operator-form tenant filters ({ in: [...] } and { eq }) before comparing them with the caller's tenant scope, and stay silent for operator forms they do not understand."
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"version": "0.365.0",
|
|
249
|
+
"type": "improvement",
|
|
250
|
+
"title": "custom-handlers and tags-basic recipes follow the coding rules",
|
|
251
|
+
"detail": "The custom-handlers recipe parses the detail row with Zod instead of casts and filters counter:active on the server (filter + totalCount) instead of in JS over one page. The tags-basic test isolates tests by tenant instead of raw DELETE statements. No runtime change."
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"version": "0.365.0",
|
|
255
|
+
"type": "improvement",
|
|
256
|
+
"title": "Recipe integration tests isolate per tenant instead of raw DELETE FROM resets",
|
|
257
|
+
"detail": "The recipe integration tests that agents learn from no longer reset tables with raw DELETE FROM statements. Each test now runs in its own tenant with its own users, or uses unique ids. The kumiko-schema-check header comment is now English. No runtime change."
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"version": "0.365.0",
|
|
261
|
+
"type": "fix",
|
|
262
|
+
"title": "Rebuilding an entity with a personal file or image field no longer fails on ciphertext from old backfill events",
|
|
263
|
+
"detail": "The earlier PII backfill encrypted file and image uuid values in stored events. Replaying those created and updated events wrote the ciphertext into the uuid column and failed with invalid input syntax. applyEntityEvent now decrypts such values through the subject KMS, writes null for an erased subject, and throws a clear InternalError when no KMS is configured. Table rows were never affected because Postgres rejects the ciphertext on a uuid column."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"version": "0.365.0",
|
|
267
|
+
"type": "fix",
|
|
268
|
+
"title": "SVG and HTML fragments are not resolved as application/xml",
|
|
269
|
+
"detail": "For uploads without a concrete content type, markup without an XML declaration is only treated as application/xml when its root element is not a known HTML or SVG element (namespace prefixes ignored). SVG logos and HTML snippets no longer route to XML ingest providers."
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"version": "0.365.0",
|
|
273
|
+
"type": "fix",
|
|
274
|
+
"title": "EventStoreExecutor.restore returns a subject_erased conflict instead of throwing",
|
|
275
|
+
"detail": "When the stored row has to be re-encrypted for a subject whose key was erased, restore now returns a failed WriteResult with the subject_erased ConflictError. The row stays soft-deleted and no restored event is appended. Code that called restore directly and caught the exception checks isSuccess instead."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"version": "0.365.0",
|
|
279
|
+
"type": "fix",
|
|
280
|
+
"title": "Retired-table scan handles DROP TABLE lists, schema-qualified names and RENAME",
|
|
281
|
+
"detail": "The scan of prior migrations for retired framework tables now marks every table in a DROP TABLE comma list as dropped, ignores a schema prefix such as \"public\".\"table\", and treats ALTER TABLE ... RENAME TO as dropping the old and creating the new name."
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"version": "0.365.0",
|
|
285
|
+
"type": "fix",
|
|
286
|
+
"title": "kumiko-schema status reports connection errors; kumiko-schema-check knows the scaffold layout",
|
|
287
|
+
"detail": "schema status now prints a one-line error with a hint and exits 1 when the database is unreachable or missing, instead of an uncaught stacktrace (the URL is never printed). kumiko-schema-check exits 0 with a pointer to kumiko-schema validate for apps whose kumiko/schema.ts derives ENTITY_METAS from APP_FEATURES. New scaffolds create the app database in docker-compose."
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"version": "0.365.0",
|
|
291
|
+
"type": "fix",
|
|
292
|
+
"title": "Retention and erasure writes skip the select-option rule",
|
|
293
|
+
"detail": "Framework system-user updates that name a writePurpose (retention, erasure) are no longer blocked by conditional select options. The null-clear comparison helper isUnchangedValue moved to engine/screen-helpers and is exported from ui-types so the renderer form and the executor share one definition."
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"version": "0.365.0",
|
|
297
|
+
"type": "fix",
|
|
298
|
+
"title": "Mid-stream token expiry returns 401 unauthenticated",
|
|
299
|
+
"detail": "A stream dispatched with a token that expires while the stream is open now ends with UnauthenticatedError (401) instead of AccessDeniedError (403), so clients show the session-ended hint. Access revocation mid-stream still returns 403 access_denied."
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"version": "0.365.0",
|
|
303
|
+
"type": "fix",
|
|
304
|
+
"title": "A where.tenantId of { in: [...] } or { eq } narrows TenantDb reads instead of being ignored",
|
|
305
|
+
"detail": "TenantDb.readWhere only understood a plain id or an array. An operator form fell back to the full read scope (own tenant plus reference rows), so { in: [ownTenant] } still returned SYSTEM reference rows. It now unwraps { in } and { eq } with the same helper the read steps use for their narrowing warning. Ids outside the caller's scope are still dropped and never widen the read."
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"version": "0.365.0",
|
|
309
|
+
"type": "fix",
|
|
310
|
+
"title": "tenantlessAnonymous is blocked by an X-Tenant header only, not by the kumiko_tenant cookie",
|
|
311
|
+
"detail": "An earlier release note said a tenantlessAnonymous handler never runs when the client supplies X-Tenant or kumiko_tenant. Only the X-Tenant header blocks it. An ambient kumiko_tenant cookie does not, because the tenant resolver ignores the cookie on this path anyway."
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"version": "0.365.0",
|
|
315
|
+
"type": "fix",
|
|
316
|
+
"title": "Test-stack rate limiting follows appContext.redis when rateLimitNowMs is set",
|
|
317
|
+
"detail": "With rateLimitNowMs the test stack builds its own rate-limit resolver; it now uses the redis instance passed via the app context instead of always the internal test redis, so buckets prefilled or cleared through the app redis take effect."
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"version": "0.365.0",
|
|
321
|
+
"type": "fix",
|
|
322
|
+
"title": "The ZIP plausibility check picks the end record whose comment length reaches the end of the file",
|
|
323
|
+
"detail": "The scan took the last end-of-central-directory signature, so a signature inside the archive comment could point the check at forged entry names. A candidate now only counts when its offset plus 22 bytes plus its comment length equals the file length. A docx upload with bytes appended after the end record is therefore rejected as content_mismatch."
|
|
324
|
+
},
|
|
2
325
|
{
|
|
3
326
|
"version": "0.364.0",
|
|
4
327
|
"type": "fix",
|