@cosmicdrift/kumiko-framework 0.371.1 → 0.373.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 +4 -1
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/embedded-entity-form.md +1 -1
- package/agents/examples/pdf-audit-export.md +1 -1
- package/agents/examples/rate-limiting.md +65 -1
- package/agents/examples/user-data-rights.md +176 -0
- package/dist/api/jwt.js +2 -1
- package/dist/api/observability-middleware.d.ts +1 -2
- package/dist/api/observability-middleware.js +9 -8
- package/dist/api/pii-leak-guard.js +2 -1
- package/dist/api/route-label.d.ts +3 -0
- package/dist/api/route-label.js +17 -0
- package/dist/api/server.d.ts +1 -2
- package/dist/api/server.js +3 -5
- package/dist/config/index.d.ts +5 -0
- package/dist/db/event-store-executor.d.ts +2 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/db/located-timestamp.d.ts +7 -0
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +8 -2
- package/dist/db/table-builder.d.ts +45 -11
- package/dist/db/tenant-db.js +3 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +47 -16
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/factories.d.ts +2 -1
- package/dist/engine/factories.js +2 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +8 -8
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +150 -6
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/membership-roles.d.ts +1 -0
- package/dist/engine/membership-roles.js +8 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +30 -2
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/types/index.js +1 -1
- package/dist/entrypoint/index.d.ts +1 -0
- package/dist/entrypoint/index.js +0 -2
- package/dist/files/assert-file-refs-belong-to-tenant.js +27 -1
- package/dist/files/bind-pending-file-refs.d.ts +31 -1
- package/dist/files/bind-pending-file-refs.js +80 -6
- package/dist/files/create-file-ref-from-bytes.js +12 -6
- package/dist/files/create-file-ref.d.ts +20 -2
- package/dist/files/create-file-ref.js +10 -1
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +25 -0
- package/dist/files/file-handle.d.ts +4 -1
- package/dist/files/file-handle.js +30 -4
- package/dist/files/file-ref-entity.d.ts +7 -7
- package/dist/files/file-ref-table.d.ts +7 -7
- package/dist/files/file-routes.js +6 -1
- package/dist/files/index.d.ts +5 -1
- package/dist/files/index.js +4 -1
- package/dist/files/load-save-file-refs.js +3 -1
- package/dist/files/provider-resolver.js +3 -1
- package/dist/files/sweep-orphaned-uploads.d.ts +16 -0
- package/dist/files/sweep-orphaned-uploads.js +53 -0
- package/dist/files/upload-ledger.d.ts +10 -0
- package/dist/files/upload-ledger.js +47 -0
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/request-locale.d.ts +15 -4
- package/dist/i18n/request-locale.js +26 -9
- package/dist/jobs/job-runner.js +2 -4
- package/dist/pipeline/dispatch-shared.js +57 -39
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/rate-limit/index.d.ts +2 -1
- package/dist/rate-limit/index.js +1 -0
- package/dist/rate-limit/resolver.d.ts +1 -1
- package/dist/rate-limit/resolver.js +104 -2
- package/dist/rate-limit/unlimited-resolver.d.ts +2 -0
- package/dist/rate-limit/unlimited-resolver.js +22 -0
- package/dist/schema-cli.js +46 -5
- package/package.json +5 -5
- package/src/changes.json +55 -0
- package/src/scripts/codemod/README.md +22 -0
- package/src/scripts/codemod/remove-engine-payload-casts.ts +318 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
2
|
-
import { RateLimitError } from "../errors/index.js";
|
|
2
|
+
import { InternalError, RateLimitError } from "../errors/index.js";
|
|
3
3
|
import { RedisKeys } from "../pipeline/redis-keys.js";
|
|
4
4
|
import { toPublicBucketName } from "./bucket.js";
|
|
5
5
|
// Token-Bucket rate limiter, atomic via Redis Lua. One round-trip per
|
|
@@ -105,6 +105,64 @@ end
|
|
|
105
105
|
|
|
106
106
|
return { math.floor(tokens), retryAfterMs }
|
|
107
107
|
`;
|
|
108
|
+
// All-or-nothing variant for several buckets. Every bucket is refilled and
|
|
109
|
+
// checked first; tokens are deducted only when each one has enough, so a
|
|
110
|
+
// rejected request charges no bucket. Multi-key EVAL needs all keys in one
|
|
111
|
+
// hash slot on Redis Cluster; the framework uses standalone Redis.
|
|
112
|
+
//
|
|
113
|
+
// KEYS[i] — bucket keys
|
|
114
|
+
// ARGV[1] — nowMs
|
|
115
|
+
// ARGV[2 + 4(i-1) .. 5 + 4(i-1)] — limit, refillRatePerMs, cost, ttlSeconds of KEYS[i]
|
|
116
|
+
//
|
|
117
|
+
// Returns the flat list { allAllowed (1|0), remaining_1, retryAfterMs_1, remaining_2, ... }.
|
|
118
|
+
// retryAfterMs_i is 0 for buckets that had enough tokens.
|
|
119
|
+
const TOKEN_BUCKET_ALL_LUA = `
|
|
120
|
+
local nowMs = tonumber(ARGV[1])
|
|
121
|
+
local count = #KEYS
|
|
122
|
+
local tokens = {}
|
|
123
|
+
local costs = {}
|
|
124
|
+
local retries = {}
|
|
125
|
+
local allAllowed = 1
|
|
126
|
+
|
|
127
|
+
for i = 1, count do
|
|
128
|
+
local base = 2 + (i - 1) * 4
|
|
129
|
+
local limit = tonumber(ARGV[base])
|
|
130
|
+
local refillRatePerMs = tonumber(ARGV[base + 1])
|
|
131
|
+
costs[i] = tonumber(ARGV[base + 2])
|
|
132
|
+
|
|
133
|
+
local data = redis.call('HMGET', KEYS[i], 'tokens', 'ts')
|
|
134
|
+
local current = tonumber(data[1])
|
|
135
|
+
local ts = tonumber(data[2])
|
|
136
|
+
if current == nil then
|
|
137
|
+
current = limit
|
|
138
|
+
else
|
|
139
|
+
local elapsed = math.max(0, nowMs - ts)
|
|
140
|
+
current = math.min(limit, current + elapsed * refillRatePerMs)
|
|
141
|
+
end
|
|
142
|
+
tokens[i] = current
|
|
143
|
+
|
|
144
|
+
if current >= costs[i] then
|
|
145
|
+
retries[i] = 0
|
|
146
|
+
else
|
|
147
|
+
retries[i] = math.ceil((costs[i] - current) / refillRatePerMs)
|
|
148
|
+
allAllowed = 0
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
local result = { allAllowed }
|
|
153
|
+
for i = 1, count do
|
|
154
|
+
if allAllowed == 1 then
|
|
155
|
+
tokens[i] = tokens[i] - costs[i]
|
|
156
|
+
end
|
|
157
|
+
local base = 2 + (i - 1) * 4
|
|
158
|
+
redis.call('HMSET', KEYS[i], 'tokens', tokens[i], 'ts', nowMs)
|
|
159
|
+
redis.call('EXPIRE', KEYS[i], tonumber(ARGV[base + 3]))
|
|
160
|
+
result[#result + 1] = math.floor(tokens[i])
|
|
161
|
+
result[#result + 1] = retries[i]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
`;
|
|
108
166
|
const REGISTERED = new WeakSet();
|
|
109
167
|
function ensureCommand(redis) {
|
|
110
168
|
if (!REGISTERED.has(redis)) {
|
|
@@ -116,6 +174,7 @@ function ensureCommand(redis) {
|
|
|
116
174
|
numberOfKeys: 1,
|
|
117
175
|
lua: TOKEN_BUCKET_PEEK_LUA,
|
|
118
176
|
});
|
|
177
|
+
redis.defineCommand("kumikoRateLimitAll", { lua: TOKEN_BUCKET_ALL_LUA });
|
|
119
178
|
REGISTERED.add(redis);
|
|
120
179
|
}
|
|
121
180
|
return redis; // @cast-boundary engine-bridge
|
|
@@ -154,6 +213,49 @@ export function createRateLimitResolver(opts) {
|
|
|
154
213
|
resetAt: decision.resetAt.toString(),
|
|
155
214
|
});
|
|
156
215
|
}
|
|
216
|
+
async function enforceAll(requests) {
|
|
217
|
+
if (requests.length === 0)
|
|
218
|
+
return [];
|
|
219
|
+
const nowMs = now();
|
|
220
|
+
const keys = requests.map(({ bucket }) => `${prefix}${bucket}`);
|
|
221
|
+
// The script writes each key back once, so a repeated key would deduct only one of its costs.
|
|
222
|
+
const duplicate = keys.find((key, index) => keys.indexOf(key) !== index);
|
|
223
|
+
if (duplicate !== undefined) {
|
|
224
|
+
throw new InternalError({
|
|
225
|
+
message: `enforceAll received the bucket "${duplicate.slice(prefix.length)}" more than once — merge the requests into one.`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const perBucketArgs = requests.flatMap(({ config }) => [
|
|
229
|
+
String(config.limit),
|
|
230
|
+
String(config.limit / (config.windowSeconds * 1000)),
|
|
231
|
+
String(config.cost ?? 1),
|
|
232
|
+
String(Math.max(1, config.windowSeconds * 2)),
|
|
233
|
+
]);
|
|
234
|
+
const [allAllowedFlag, ...flat] = await client.kumikoRateLimitAll(keys.length, ...keys, String(nowMs), ...perBucketArgs);
|
|
235
|
+
const outcomes = requests.map(({ bucket, config }, index) => {
|
|
236
|
+
const retryAfterMs = flat[index * 2 + 1] ?? 0;
|
|
237
|
+
const decision = {
|
|
238
|
+
allowed: retryAfterMs === 0,
|
|
239
|
+
limit: config.limit,
|
|
240
|
+
remaining: flat[index * 2] ?? 0,
|
|
241
|
+
retryAfterSeconds: Math.ceil(retryAfterMs / 1000),
|
|
242
|
+
windowSeconds: config.windowSeconds,
|
|
243
|
+
resetAt: Temporal.Instant.fromEpochMilliseconds(nowMs + retryAfterMs),
|
|
244
|
+
};
|
|
245
|
+
return { bucket, retryAfterMs, decision };
|
|
246
|
+
});
|
|
247
|
+
if (allAllowedFlag === 1)
|
|
248
|
+
return outcomes.map(({ decision }) => decision);
|
|
249
|
+
const rejected = outcomes.reduce((worst, outcome) => outcome.retryAfterMs > worst.retryAfterMs ? outcome : worst);
|
|
250
|
+
throw new RateLimitError({
|
|
251
|
+
bucket: toPublicBucketName(rejected.bucket),
|
|
252
|
+
limit: rejected.decision.limit,
|
|
253
|
+
windowSeconds: rejected.decision.windowSeconds,
|
|
254
|
+
remaining: rejected.decision.remaining,
|
|
255
|
+
retryAfterSeconds: rejected.decision.retryAfterSeconds,
|
|
256
|
+
resetAt: rejected.decision.resetAt.toString(),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
157
259
|
async function peek(bucket, config) {
|
|
158
260
|
const refillRatePerMs = config.limit / (config.windowSeconds * 1000);
|
|
159
261
|
const nowMs = now();
|
|
@@ -171,5 +273,5 @@ export function createRateLimitResolver(opts) {
|
|
|
171
273
|
resetAt,
|
|
172
274
|
};
|
|
173
275
|
}
|
|
174
|
-
return { check, enforce, peek };
|
|
276
|
+
return { check, enforce, enforceAll, peek };
|
|
175
277
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
2
|
+
// Resolver for callers that are bootstrap work rather than tenant traffic (boot seeds,
|
|
3
|
+
// seed migrations): a handler's `rateLimit` declaration must not fail them, and they must not
|
|
4
|
+
// drain the buckets real requests share. Nothing is stored, so the decision is always "full bucket".
|
|
5
|
+
export function createUnlimitedRateLimitResolver() {
|
|
6
|
+
function fullBucket(config) {
|
|
7
|
+
return {
|
|
8
|
+
allowed: true,
|
|
9
|
+
limit: config.limit,
|
|
10
|
+
remaining: config.limit,
|
|
11
|
+
retryAfterSeconds: 0,
|
|
12
|
+
windowSeconds: config.windowSeconds,
|
|
13
|
+
resetAt: Temporal.Instant.fromEpochMilliseconds(Date.now()),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
check: async (_bucket, config) => fullBucket(config),
|
|
18
|
+
enforce: async (_bucket, config) => fullBucket(config),
|
|
19
|
+
enforceAll: async (requests) => requests.map(({ config }) => fullBucket(config)),
|
|
20
|
+
peek: async (_bucket, config) => fullBucket(config),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/schema-cli.js
CHANGED
|
@@ -35,6 +35,41 @@ function withFileStoragePresence(env) {
|
|
|
35
35
|
? { ...env, [FILE_STORAGE_PROVIDER_ENV]: FILE_STORAGE_PROVIDER_BOOT_SENTINEL }
|
|
36
36
|
: { ...env };
|
|
37
37
|
}
|
|
38
|
+
function isStringSet(value) {
|
|
39
|
+
return value instanceof Set && [...value].every((entry) => typeof entry === "string");
|
|
40
|
+
}
|
|
41
|
+
function isBoolean(value) {
|
|
42
|
+
return typeof value === "boolean";
|
|
43
|
+
}
|
|
44
|
+
function validateBootOptionsError(detail) {
|
|
45
|
+
return new Error(`kumiko/schema.ts: \`VALIDATE_BOOT_OPTIONS\` ${detail}`);
|
|
46
|
+
}
|
|
47
|
+
function readOptionalOption(raw, key, isValid, expected) {
|
|
48
|
+
const value = Reflect.get(raw, key);
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!isValid(value))
|
|
52
|
+
throw validateBootOptionsError(`\`${key}\` must be ${expected}.`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
// Boundary check for `export const VALIDATE_BOOT_OPTIONS` in kumiko/schema.ts.
|
|
56
|
+
// Only the options that change what validateBoot reports are honoured; `env` and
|
|
57
|
+
// `entityFieldCipher` are runner-injected and deliberately ignored here.
|
|
58
|
+
function parseValidateBootOptions(raw) {
|
|
59
|
+
if (raw === undefined)
|
|
60
|
+
return {};
|
|
61
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
62
|
+
throw validateBootOptionsError("must be an object (ValidateBootOptions).");
|
|
63
|
+
}
|
|
64
|
+
const navAllowlist = readOptionalOption(raw, "navAllowlist", isStringSet, "a Set<string>");
|
|
65
|
+
const navAllowlistExempt = readOptionalOption(raw, "navAllowlistExempt", isStringSet, "a Set<string>");
|
|
66
|
+
const warnOnUniqueAccessRoles = readOptionalOption(raw, "warnOnUniqueAccessRoles", isBoolean, "a boolean");
|
|
67
|
+
return {
|
|
68
|
+
...(navAllowlist !== undefined && { navAllowlist }),
|
|
69
|
+
...(navAllowlistExempt !== undefined && { navAllowlistExempt }),
|
|
70
|
+
...(warnOnUniqueAccessRoles !== undefined && { warnOnUniqueAccessRoles }),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
38
73
|
// Single load-path for kumiko/schema.ts (512/3) — `generate` and `validate`
|
|
39
74
|
// both need ENTITY_METAS (and validate additionally wants FEATURES); this is
|
|
40
75
|
// the one place that enforces "ENTITY_METAS must be an array" so the two
|
|
@@ -42,13 +77,17 @@ function withFileStoragePresence(env) {
|
|
|
42
77
|
// want a graceful exit code (validate) catch it themselves.
|
|
43
78
|
async function loadSchemaModFromApp(schemaFile) {
|
|
44
79
|
// bun imports TS directly — no spawn needed.
|
|
45
|
-
const mod =
|
|
46
|
-
if (!Array.isArray(mod
|
|
80
|
+
const mod = await import(__rewriteRelativeImportExtension(schemaFile));
|
|
81
|
+
if (!Array.isArray(mod["ENTITY_METAS"])) {
|
|
47
82
|
throw new Error(`Schema file ${schemaFile} muss \`export const ENTITY_METAS: EntityTableMeta[]\` haben.`);
|
|
48
83
|
}
|
|
49
|
-
const entityMetas = mod
|
|
84
|
+
const entityMetas = mod["ENTITY_METAS"];
|
|
50
85
|
assertUniqueTableNames(entityMetas);
|
|
51
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
entityMetas,
|
|
88
|
+
features: mod["FEATURES"],
|
|
89
|
+
validateBootOptions: mod["VALIDATE_BOOT_OPTIONS"],
|
|
90
|
+
};
|
|
52
91
|
}
|
|
53
92
|
function nextSequenceNumber(migrationsDir) {
|
|
54
93
|
if (!existsSync(migrationsDir))
|
|
@@ -201,7 +240,8 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
201
240
|
// 1. schema drift: would `generate` write a migration? (= an entity was
|
|
202
241
|
// added/changed but never generated → missing table → prod 500)
|
|
203
242
|
// 2. boot validity: validateBoot over the composed FEATURES (QN/screen/
|
|
204
|
-
// nav/role refs). Runs only if kumiko/schema.ts exports FEATURES
|
|
243
|
+
// nav/role refs). Runs only if kumiko/schema.ts exports FEATURES;
|
|
244
|
+
// honours its optional VALIDATE_BOOT_OPTIONS export.
|
|
205
245
|
// 3. migration-content drift: replay every committed *.sql file and
|
|
206
246
|
// diff the result against .snapshot.json — catches a migration
|
|
207
247
|
// file whose SQL body doesn't match what its own snapshot entry
|
|
@@ -257,6 +297,7 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
257
297
|
if (Array.isArray(mod.FEATURES)) {
|
|
258
298
|
try {
|
|
259
299
|
validateBoot(mod.FEATURES, {
|
|
300
|
+
...parseValidateBootOptions(schemaMod.validateBootOptions),
|
|
260
301
|
env: withFileStoragePresence(process.env),
|
|
261
302
|
});
|
|
262
303
|
out.log(" ✓ boot: feature configuration is valid");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.373.0",
|
|
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>",
|
|
@@ -199,8 +199,8 @@
|
|
|
199
199
|
"./package.json": "./package.json"
|
|
200
200
|
},
|
|
201
201
|
"dependencies": {
|
|
202
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
203
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
202
|
+
"@cosmicdrift/kumiko-http": "0.373.0",
|
|
203
|
+
"@cosmicdrift/kumiko-types": "0.373.0",
|
|
204
204
|
"bullmq": "^5.76.7",
|
|
205
205
|
"bun-types": "^1.3.13",
|
|
206
206
|
"hono": "^4.13.1",
|
|
@@ -216,8 +216,8 @@
|
|
|
216
216
|
"zod": "^4.4.3"
|
|
217
217
|
},
|
|
218
218
|
"devDependencies": {
|
|
219
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
220
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
219
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.373.0",
|
|
220
|
+
"@cosmicdrift/kumiko-testing": "0.373.0",
|
|
221
221
|
"bun-types": "^1.3.13",
|
|
222
222
|
"pino-pretty": "^13.1.3"
|
|
223
223
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,59 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.373.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "loadPendingFileRef and bindPendingFileRef bind a caller's own pending upload from a custom handler",
|
|
6
|
+
"detail": "Additive. fileName is not part of PendingFileRef because it is personal data; read it via the fileRef executor's detail. Unexpected update errors throw; version_conflict and unique_violation map to \"version-conflict\"."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.373.0",
|
|
10
|
+
"type": "breaking",
|
|
11
|
+
"title": "rateLimit accepts a list of buckets that are enforced all-or-nothing",
|
|
12
|
+
"detail": "RateLimitDeclaration is now a single option, a non-empty list of options or the disabled form. RateLimitResolver gains the required method enforceAll(requests: readonly RateLimitBucketRequest[]); createRateLimitResolver and createUnlimitedRateLimitResolver implement it. Payload buckets in additionalRateLimits are charged through the same call.",
|
|
13
|
+
"migration": "Custom RateLimitResolver implementations and test fakes need an enforceAll method (check every bucket, then deduct from all or none, and throw RateLimitError for the rejected bucket with the largest retryAfter). Existing rateLimit declarations stay valid. A duplicate payloadField in additionalRateLimits is now a boot error; merge the entries into one."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.372.0",
|
|
17
|
+
"type": "improvement",
|
|
18
|
+
"title": "kumiko check validates the boot configuration with the validateBootOptions from kumiko.config.ts",
|
|
19
|
+
"detail": "Set validateBootOptions in defineAppConfig (navAllowlist, navAllowlistExempt, warnOnUniqueAccessRoles); the managed kumiko/schema.ts exports it as VALIDATE_BOOT_OPTIONS and kumiko check validates with it. Apps with a hand-written kumiko/schema.ts can export VALIDATE_BOOT_OPTIONS themselves.",
|
|
20
|
+
"migration": "Existing apps get the new line in kumiko/schema.ts through `kumiko-upgrade --apply` (managed file). The bin files belong to the app: pass `...(appConfig.validateBootOptions && { validateBootOptions: appConfig.validateBootOptions })` to runProdApp in bin/main.ts and to runDevApp in bin/server.ts (import appConfig from \"../kumiko.config\") so runtime and kumiko check agree."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"version": "0.372.0",
|
|
24
|
+
"type": "fix",
|
|
25
|
+
"title": "createEntity no longer collapses to never for inline field literals such as { type: \"reference\", entity }",
|
|
26
|
+
"detail": "The fields type parameter is const, so the literal type of each inline field survives without as const."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"version": "0.372.0",
|
|
30
|
+
"type": "breaking",
|
|
31
|
+
"title": "executor.detail() and executor.list() return EntityRow<E>, derived from the entity definition",
|
|
32
|
+
"detail": "Rows of createEntityExecutor / createEventStoreExecutor are typed per entity; wide entities keep Record<string, unknown>. createBooleanField is typed non-null. UserMfaRow in the bundled auth-mfa feature now declares enabledAt and lastUsedAt as Temporal.Instant, which is what the row always held.",
|
|
33
|
+
"migration": "Remove the `row as T` casts on executor.detail() / executor.list() results with `bun kumiko-upgrade --apply` (codemod remove-engine-payload-casts). Casts to a type the row does not satisfy stay and are listed as manual; replace them with a field-level check or a narrower type.",
|
|
34
|
+
"codemod": "scripts/codemod/remove-engine-payload-casts.ts",
|
|
35
|
+
"manualAfterCodemod": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"version": "0.372.0",
|
|
39
|
+
"type": "fix",
|
|
40
|
+
"title": "A save cannot reference a file that belongs to another record",
|
|
41
|
+
"detail": "Creating or updating a record with a file id that is already bound to a different record of the same tenant is now rejected with invalid_element on that field, the same error as for a foreign tenant. Pending uploads, ids the record already holds and moves between its own file fields are unaffected."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"version": "0.372.0",
|
|
45
|
+
"type": "breaking",
|
|
46
|
+
"title": "Tokens carrying system or all lose those roles on verify",
|
|
47
|
+
"detail": "jwt.verify strips the system and all roles that session minting never grants, so a self-signed or leaked-key token cannot act as System.",
|
|
48
|
+
"migration": "Apps that mint their own tokens with the `system` role must stop doing so and use `createSystemUser` or system dispatch in-process instead. Tokens issued by the framework are unaffected. Tests that send a system user over `stack.http.*` must switch to `stack.dispatcher.write/query`."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"version": "0.372.0",
|
|
52
|
+
"type": "breaking",
|
|
53
|
+
"title": "http.route and the metric route label carry the route pattern, http.target is removed",
|
|
54
|
+
"detail": "Spans are named \"METHOD pattern\" instead of \"http.request\"; paths no route matched are labelled \"unmatched\". The PII ciphertext guard message names the pattern as well.",
|
|
55
|
+
"migration": "Dashboards, alerts and trace queries keyed on raw paths or on the span name \"http.request\" must switch to route patterns and the \"METHOD pattern\" span name. Read the query string from your access logs if you relied on http.target."
|
|
56
|
+
},
|
|
2
57
|
{
|
|
3
58
|
"version": "0.371.0",
|
|
4
59
|
"type": "improvement",
|
|
@@ -118,3 +118,25 @@ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/import-extens
|
|
|
118
118
|
Runs via `bun kumiko-upgrade --apply`. Without a mode flag it writes. `--check` exits 1 while any
|
|
119
119
|
rewritable or manual site is left. The `import-extensions` guard (`kumiko check guards`) reports
|
|
120
120
|
sites that are still on `.js`.
|
|
121
|
+
|
|
122
|
+
## remove-engine-payload-casts.ts
|
|
123
|
+
|
|
124
|
+
`executor.detail()` and `executor.list()` from `createEntityExecutor` / `createEventStoreExecutor`
|
|
125
|
+
return the entity's own row type (`EntityRow<E>`), so `row["f"] as T`, `row as T` and
|
|
126
|
+
`(await executor.detail(…)) as unknown as T` are usually redundant. The codemod removes such a cast,
|
|
127
|
+
and its `@cast-boundary` marker comment when no other cast stays in the statement, only when the
|
|
128
|
+
operand derives from an executor read (directly, through `rows[0]`, property access, `??` or a
|
|
129
|
+
single-assignment variable) and the type checker says its type is assignable to the cast target. A
|
|
130
|
+
file is left untouched if removing the casts would add type errors to it.
|
|
131
|
+
|
|
132
|
+
It type-checks against the tsconfig at or above each file, so run it with the new framework version
|
|
133
|
+
installed. Reported as `manual` and left alone: casts the row type does not satisfy (for example a
|
|
134
|
+
nullable column cast to a non-null type, or `rows[0]` under `noUncheckedIndexedAccess`), and files
|
|
135
|
+
without a tsconfig. A call site that still needs a narrower type than the row gives keeps its cast.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/remove-engine-payload-casts.ts <dir> [--check | --dry-run | --write] [--no-format]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Runs via `bun kumiko-upgrade --apply`. Without a mode flag it writes, then formats the changed files
|
|
142
|
+
with Biome. `--check` exits 1 while any rewritable or manual site is left.
|