@jeffusion/bungee-core 5.23.1 → 5.23.3
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/dist/config-storage-worker.js +927 -393
- package/dist/main.js +2523 -619
- package/dist/plugin-state-worker.js +178 -16
- package/dist/plugins/codex-router/control.js +1 -1
- package/dist/plugins/codex-router/control.js.map +3 -3
- package/dist/plugins/codex-router/index.js +2289 -3
- package/dist/plugins/codex-router/index.js.map +17 -6
- package/dist/plugins/llm-protocol-adapter/control.js +93 -19
- package/dist/plugins/llm-protocol-adapter/control.js.map +7 -6
- package/dist/plugins/llm-protocol-adapter/index.js +131 -36
- package/dist/plugins/llm-protocol-adapter/index.js.map +10 -9
- package/dist/plugins/model-mapping/control.js +1 -1
- package/dist/plugins/model-mapping/control.js.map +3 -3
- package/dist/plugins/model-mapping/index.js +48 -17
- package/dist/plugins/model-mapping/index.js.map +6 -5
- package/dist/plugins/model-mapping/manifest.json +50 -6
- package/dist/plugins/models-dev/control.js +1 -1
- package/dist/plugins/models-dev/control.js.map +3 -3
- package/dist/plugins/models-dev/index.js +1 -1
- package/dist/plugins/models-dev/index.js.map +3 -3
- package/dist/plugins/token-stats/control.js +1 -1
- package/dist/plugins/token-stats/control.js.map +3 -3
- package/dist/plugins/token-stats/index.js +1 -1
- package/dist/plugins/token-stats/index.js.map +3 -3
- package/package.json +2 -2
|
@@ -1,61 +1,54 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
import {
|
|
4
|
-
import { mkdirSync } from "fs";
|
|
5
|
-
import { dirname } from "path";
|
|
2
|
+
// src/supervision/process-lifetime-guard.ts
|
|
3
|
+
import { dirname, join, resolve } from "path";
|
|
6
4
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
recovery;
|
|
12
|
-
name = "ConfigRepositoryError";
|
|
13
|
-
constructor(code, message, cause, recovery) {
|
|
14
|
-
super(message, { cause });
|
|
15
|
-
this.code = code;
|
|
16
|
-
this.cause = cause;
|
|
17
|
-
this.recovery = recovery;
|
|
5
|
+
// ../../node_modules/canonicalize/lib/canonicalize.js
|
|
6
|
+
function canonicalize(object, seen = new Set) {
|
|
7
|
+
if (typeof object === "number" && isNaN(object)) {
|
|
8
|
+
throw new Error("NaN is not allowed");
|
|
18
9
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// src/config-storage/sqlite-query.ts
|
|
22
|
-
function sqliteAll(db, sql, ...params) {
|
|
23
|
-
const statement = db.prepare(sql);
|
|
24
|
-
try {
|
|
25
|
-
return statement.all(...params);
|
|
26
|
-
} finally {
|
|
27
|
-
statement.finalize();
|
|
10
|
+
if (typeof object === "number" && !isFinite(object)) {
|
|
11
|
+
throw new Error("Infinity is not allowed");
|
|
28
12
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const statement = db.prepare(sql);
|
|
32
|
-
try {
|
|
33
|
-
return statement.get(...params);
|
|
34
|
-
} finally {
|
|
35
|
-
statement.finalize();
|
|
13
|
+
if (object === null || typeof object !== "object") {
|
|
14
|
+
return JSON.stringify(object);
|
|
36
15
|
}
|
|
16
|
+
if (typeof object.toJSON === "function") {
|
|
17
|
+
if (seen.has(object)) {
|
|
18
|
+
throw new Error("Circular reference detected");
|
|
19
|
+
}
|
|
20
|
+
seen.add(object);
|
|
21
|
+
const result = canonicalize(object.toJSON(), seen);
|
|
22
|
+
seen.delete(object);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
if (seen.has(object)) {
|
|
26
|
+
throw new Error("Circular reference detected");
|
|
27
|
+
}
|
|
28
|
+
seen.add(object);
|
|
29
|
+
let result;
|
|
30
|
+
if (Array.isArray(object)) {
|
|
31
|
+
const values = object.map((cv) => {
|
|
32
|
+
const value = cv === undefined || typeof cv === "symbol" ? null : cv;
|
|
33
|
+
return canonicalize(value, seen);
|
|
34
|
+
});
|
|
35
|
+
result = `[${values.join(",")}]`;
|
|
36
|
+
} else {
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const key of Object.keys(object).sort()) {
|
|
39
|
+
if (object[key] === undefined || typeof object[key] === "symbol") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
parts.push(`${canonicalize(key)}:${canonicalize(object[key], seen)}`);
|
|
43
|
+
}
|
|
44
|
+
result = `{${parts.join(",")}}`;
|
|
45
|
+
}
|
|
46
|
+
seen.delete(object);
|
|
47
|
+
return result;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
// src/config-storage/
|
|
40
|
-
|
|
41
|
-
var BODY_PARSER_LIMIT = /^([1-9][0-9]*)(b|kb|mb|gb)$/;
|
|
42
|
-
var BODY_SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
|
|
43
|
-
function isLogLevel(value) {
|
|
44
|
-
return LOG_LEVELS.has(value);
|
|
45
|
-
}
|
|
46
|
-
function isBodyParserLimit(value) {
|
|
47
|
-
const match = BODY_PARSER_LIMIT.exec(value);
|
|
48
|
-
if (match === null)
|
|
49
|
-
return false;
|
|
50
|
-
const numeric = match[1];
|
|
51
|
-
return numeric !== undefined && Number.isSafeInteger(Number(numeric));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// src/config-storage/plugin-name.ts
|
|
55
|
-
var PLUGIN_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
56
|
-
function isPluginName(value) {
|
|
57
|
-
return PLUGIN_NAME_PATTERN.test(value);
|
|
58
|
-
}
|
|
50
|
+
// src/config-storage/json-preflight.ts
|
|
51
|
+
import { isProxy } from "util/types";
|
|
59
52
|
|
|
60
53
|
// src/config-storage/validation.ts
|
|
61
54
|
var LOWERCASE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
@@ -181,48 +174,252 @@ function rejectUnknownFields(object, allowed, path, context) {
|
|
|
181
174
|
}
|
|
182
175
|
}
|
|
183
176
|
|
|
184
|
-
// src/config-storage/
|
|
185
|
-
function
|
|
186
|
-
|
|
177
|
+
// src/config-storage/utf16.ts
|
|
178
|
+
function hasLoneSurrogate(value) {
|
|
179
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
180
|
+
const code = value.charCodeAt(index);
|
|
181
|
+
if (code >= 55296 && code <= 56319) {
|
|
182
|
+
if (index + 1 >= value.length)
|
|
183
|
+
return true;
|
|
184
|
+
const next = value.charCodeAt(index + 1);
|
|
185
|
+
if (next < 56320 || next > 57343)
|
|
186
|
+
return true;
|
|
187
|
+
index += 1;
|
|
188
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
187
193
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
|
|
195
|
+
// src/config-storage/json-preflight.ts
|
|
196
|
+
var FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
197
|
+
function childPath(parent, key) {
|
|
198
|
+
return parent ? `${parent}.${key}` : key;
|
|
199
|
+
}
|
|
200
|
+
function symbolPath(parent, key) {
|
|
201
|
+
const segment = `[Symbol(${key.description ?? ""})]`;
|
|
202
|
+
return `${parent}${segment}`;
|
|
203
|
+
}
|
|
204
|
+
function addInvalid(context, path) {
|
|
205
|
+
context.add("non_json_value", path, "Expected a JSON-compatible value");
|
|
206
|
+
}
|
|
207
|
+
function isCanonicalArrayIndex(key) {
|
|
208
|
+
if (!/^(?:0|[1-9]\d*)$/.test(key))
|
|
209
|
+
return false;
|
|
210
|
+
const index = Number(key);
|
|
211
|
+
return Number.isInteger(index) && index >= 0 && index <= 4294967294 && String(index) === key;
|
|
212
|
+
}
|
|
213
|
+
function inspectObject(pending, stack, seen, context) {
|
|
214
|
+
const source = pending.value;
|
|
215
|
+
let array;
|
|
216
|
+
let prototype;
|
|
217
|
+
let properties;
|
|
218
|
+
let arrayLength = 0;
|
|
219
|
+
try {
|
|
220
|
+
if (isProxy(source))
|
|
221
|
+
throw new TypeError("Proxy objects are not JSON values");
|
|
222
|
+
if (seen.has(source)) {
|
|
223
|
+
addInvalid(context, pending.path);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
seen.add(source);
|
|
227
|
+
array = Array.isArray(source);
|
|
228
|
+
prototype = Object.getPrototypeOf(source);
|
|
229
|
+
properties = Reflect.ownKeys(source).map((key) => {
|
|
230
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
231
|
+
if (!descriptor)
|
|
232
|
+
throw new TypeError("Missing own property descriptor");
|
|
233
|
+
return { key, descriptor };
|
|
234
|
+
});
|
|
235
|
+
if (array) {
|
|
236
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, "length");
|
|
237
|
+
if (!descriptor || typeof descriptor.value !== "number") {
|
|
238
|
+
throw new TypeError("Invalid array length descriptor");
|
|
239
|
+
}
|
|
240
|
+
arrayLength = descriptor.value;
|
|
241
|
+
}
|
|
242
|
+
if (Object.getPrototypeOf(source) !== prototype) {
|
|
243
|
+
throw new TypeError("Prototype changed during reflection");
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
addInvalid(context, pending.path);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!array && prototype !== Object.prototype && prototype !== null || array && prototype !== Array.prototype) {
|
|
250
|
+
addInvalid(context, pending.path);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const target = array ? [] : {};
|
|
254
|
+
pending.assign(target);
|
|
255
|
+
const children = [];
|
|
256
|
+
const indexes = [];
|
|
257
|
+
for (const { key, descriptor } of properties) {
|
|
258
|
+
if (array && key === "length")
|
|
259
|
+
continue;
|
|
260
|
+
if (typeof key === "symbol") {
|
|
261
|
+
addInvalid(context, symbolPath(pending.path, key));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const propertyName = String(key);
|
|
265
|
+
const arrayIndex = array && isCanonicalArrayIndex(propertyName);
|
|
266
|
+
const path = arrayIndex ? `${pending.path}[${propertyName}]` : childPath(pending.path, propertyName);
|
|
267
|
+
if (arrayIndex)
|
|
268
|
+
indexes.push(Number(propertyName));
|
|
269
|
+
if (array && !arrayIndex) {
|
|
270
|
+
addInvalid(context, path);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (hasLoneSurrogate(propertyName))
|
|
274
|
+
addInvalid(context, path);
|
|
275
|
+
if (!descriptor.enumerable || !("value" in descriptor)) {
|
|
276
|
+
addInvalid(context, path);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (FORBIDDEN_KEYS.has(propertyName)) {
|
|
280
|
+
context.add("forbidden_key", path, "Key is forbidden in JSON configuration");
|
|
281
|
+
children.push({ value: descriptor.value, path, assign() {} });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
children.push({
|
|
285
|
+
value: descriptor.value,
|
|
286
|
+
path,
|
|
287
|
+
assign(value) {
|
|
288
|
+
if (array)
|
|
289
|
+
target[Number(propertyName)] = value;
|
|
290
|
+
else
|
|
291
|
+
Object.defineProperty(target, propertyName, {
|
|
292
|
+
value,
|
|
293
|
+
enumerable: true,
|
|
294
|
+
writable: true,
|
|
295
|
+
configurable: true
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (array && indexes.length !== arrayLength) {
|
|
301
|
+
indexes.sort((left, right) => left - right);
|
|
302
|
+
let expected = 0;
|
|
303
|
+
for (const index of indexes) {
|
|
304
|
+
if (index !== expected)
|
|
305
|
+
break;
|
|
306
|
+
expected += 1;
|
|
307
|
+
}
|
|
308
|
+
addInvalid(context, `${pending.path}[${expected}]`);
|
|
309
|
+
}
|
|
310
|
+
for (let index = children.length - 1;index >= 0; index -= 1) {
|
|
311
|
+
const child = children[index];
|
|
312
|
+
if (child !== undefined)
|
|
313
|
+
stack.push(child);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
class JsonSnapshotError extends Error {
|
|
318
|
+
errors;
|
|
319
|
+
name = "JsonSnapshotError";
|
|
320
|
+
constructor(errors) {
|
|
321
|
+
super("Value is not a side-effect-free JSON graph");
|
|
322
|
+
this.errors = errors;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function snapshotJsonGraph(input) {
|
|
326
|
+
const context = new ValidationContext;
|
|
327
|
+
const value = preflightJsonGraph(input, context);
|
|
328
|
+
if (value === undefined || context.errors.length > 0) {
|
|
329
|
+
throw new JsonSnapshotError(context.errors);
|
|
330
|
+
}
|
|
191
331
|
return value;
|
|
192
332
|
}
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
333
|
+
function preflightJsonGraph(input, context) {
|
|
334
|
+
let output;
|
|
335
|
+
const stack = [{ value: input, path: "", assign(value) {
|
|
336
|
+
output = value;
|
|
337
|
+
} }];
|
|
338
|
+
const seen = new WeakSet;
|
|
339
|
+
while (stack.length) {
|
|
340
|
+
const pending = stack.pop();
|
|
341
|
+
if (pending === undefined)
|
|
342
|
+
break;
|
|
343
|
+
const value = pending.value;
|
|
344
|
+
if (value === null || typeof value === "boolean")
|
|
345
|
+
pending.assign(value);
|
|
346
|
+
else if (typeof value === "string") {
|
|
347
|
+
if (hasLoneSurrogate(value))
|
|
348
|
+
addInvalid(context, pending.path);
|
|
349
|
+
else
|
|
350
|
+
pending.assign(value);
|
|
351
|
+
} else if (typeof value === "number") {
|
|
352
|
+
if (Number.isFinite(value))
|
|
353
|
+
pending.assign(value);
|
|
354
|
+
else
|
|
355
|
+
addInvalid(context, pending.path);
|
|
356
|
+
} else if (typeof value === "object")
|
|
357
|
+
inspectObject(pending, stack, seen, context);
|
|
358
|
+
else
|
|
359
|
+
addInvalid(context, pending.path);
|
|
360
|
+
}
|
|
361
|
+
return context.errors.length ? undefined : output;
|
|
197
362
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
363
|
+
|
|
364
|
+
// src/config-storage/content-hash.ts
|
|
365
|
+
class ConfigurationHashError extends Error {
|
|
366
|
+
cause;
|
|
367
|
+
name = "ConfigurationHashError";
|
|
368
|
+
constructor(cause) {
|
|
369
|
+
super("RFC 8785 canonicalization failed", { cause });
|
|
370
|
+
this.cause = cause;
|
|
371
|
+
}
|
|
202
372
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
373
|
+
function canonicalJson(value) {
|
|
374
|
+
try {
|
|
375
|
+
const result = canonicalize(snapshotJsonGraph(value));
|
|
376
|
+
if (result === undefined)
|
|
377
|
+
throw new TypeError("canonicalize returned undefined");
|
|
378
|
+
return result;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
throw new ConfigurationHashError(error);
|
|
381
|
+
}
|
|
207
382
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return
|
|
383
|
+
function hashConfigurationContent(value) {
|
|
384
|
+
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
385
|
+
const hex = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
386
|
+
return `sha256:${hex}`;
|
|
212
387
|
}
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
388
|
+
function hashConfigurationRequest(identity) {
|
|
389
|
+
return hashConfigurationContent({
|
|
390
|
+
kind: identity.kind,
|
|
391
|
+
expected_revision: identity.expected_revision,
|
|
392
|
+
aggregate: identity.aggregate,
|
|
393
|
+
target_worker_slots: [...identity.target_worker_slots].sort((left, right) => left - right)
|
|
394
|
+
});
|
|
220
395
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
396
|
+
|
|
397
|
+
// src/supervision/process-lifetime-guard.ts
|
|
398
|
+
var PROCESS_LIFETIME_GUARD_DOMAIN = "bungee-local-flock";
|
|
399
|
+
function parseProcessLifetimeGuardFacts(value) {
|
|
400
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
401
|
+
throw new Error("process lifetime guard facts are malformed");
|
|
402
|
+
const facts = value;
|
|
403
|
+
const keys = ["schema", "identity", "process_instance_id", "path", "device", "inode", "host_boot_id", "domain"];
|
|
404
|
+
if (Object.keys(facts).length !== keys.length || keys.some((key) => !Object.hasOwn(facts, key)) || facts.schema !== "bungee-process-lifetime-guard-v1" || facts.domain !== PROCESS_LIFETIME_GUARD_DOMAIN || typeof facts.identity !== "string" || !isLowercaseUuid(facts.identity) || typeof facts.process_instance_id !== "string" || !isLowercaseUuid(facts.process_instance_id) || typeof facts.path !== "string" || facts.path !== resolve(facts.path) || typeof facts.device !== "string" || !/^\d+$/.test(facts.device) || typeof facts.inode !== "string" || !/^[1-9]\d*$/.test(facts.inode) || typeof facts.host_boot_id !== "string" || !/^(linux:[0-9a-f-]{36}|darwin:\d+:\d+)$/.test(facts.host_boot_id)) {
|
|
405
|
+
throw new Error("process lifetime guard facts are malformed");
|
|
406
|
+
}
|
|
407
|
+
return Object.freeze({ ...facts });
|
|
408
|
+
}
|
|
409
|
+
var inheritedDescriptors = new Map;
|
|
410
|
+
|
|
411
|
+
// src/config-storage/repository-types.ts
|
|
412
|
+
class ConfigRepositoryError extends Error {
|
|
413
|
+
code;
|
|
414
|
+
cause;
|
|
415
|
+
recovery;
|
|
416
|
+
name = "ConfigRepositoryError";
|
|
417
|
+
constructor(code, message, cause, recovery) {
|
|
418
|
+
super(message, { cause });
|
|
419
|
+
this.code = code;
|
|
420
|
+
this.cause = cause;
|
|
421
|
+
this.recovery = recovery;
|
|
422
|
+
}
|
|
226
423
|
}
|
|
227
424
|
|
|
228
425
|
// ../types/src/daemon-control.ts
|
|
@@ -6542,6 +6739,21 @@ function validateRouteDomain(object, path, hasService, endpointCount, context) {
|
|
|
6542
6739
|
}
|
|
6543
6740
|
}
|
|
6544
6741
|
|
|
6742
|
+
// src/config-storage/global-scalars.ts
|
|
6743
|
+
var LOG_LEVELS = new Set(["trace", "debug", "info", "warn", "error", "fatal"]);
|
|
6744
|
+
var BODY_PARSER_LIMIT = /^([1-9][0-9]*)(b|kb|mb|gb)$/;
|
|
6745
|
+
var BODY_SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
|
|
6746
|
+
function isLogLevel(value) {
|
|
6747
|
+
return LOG_LEVELS.has(value);
|
|
6748
|
+
}
|
|
6749
|
+
function isBodyParserLimit(value) {
|
|
6750
|
+
const match = BODY_PARSER_LIMIT.exec(value);
|
|
6751
|
+
if (match === null)
|
|
6752
|
+
return false;
|
|
6753
|
+
const numeric = match[1];
|
|
6754
|
+
return numeric !== undefined && Number.isSafeInteger(Number(numeric));
|
|
6755
|
+
}
|
|
6756
|
+
|
|
6545
6757
|
// src/config-storage/global-validation.ts
|
|
6546
6758
|
function validateGlobalPolicies(object, context) {
|
|
6547
6759
|
if (object.log_level !== undefined && (typeof object.log_level !== "string" || !isLogLevel(object.log_level))) {
|
|
@@ -6555,208 +6767,24 @@ function validateGlobalPolicies(object, context) {
|
|
|
6555
6767
|
for (const error of validatePublicationPolicy(object.publication)) {
|
|
6556
6768
|
const path = error.field === "publication" ? "publication" : `publication.${error.field}`;
|
|
6557
6769
|
const code = error.message === "Required field is missing" ? "required" : error.message.startsWith("Expected") || error.field === "publication" ? "invalid_type" : "invalid_value";
|
|
6558
|
-
context.add(code, path, error.message);
|
|
6559
|
-
}
|
|
6560
|
-
}
|
|
6561
|
-
const logging = objectField(object.logging, "logging", ["body"], context);
|
|
6562
|
-
if (!logging || logging.body === undefined)
|
|
6563
|
-
return;
|
|
6564
|
-
const body = objectField(logging.body, "logging.body", ["enabled", "max_size", "retention_days"], context);
|
|
6565
|
-
if (!body)
|
|
6566
|
-
return;
|
|
6567
|
-
booleanField(body, "enabled", "logging.body", context);
|
|
6568
|
-
numberField(body, "max_size", "logging.body", context, { required: true, positive: true });
|
|
6569
|
-
numberField(body, "retention_days", "logging.body", context, { required: true, positive: true });
|
|
6570
|
-
}
|
|
6571
|
-
|
|
6572
|
-
// src/config-storage/json-preflight.ts
|
|
6573
|
-
import { isProxy } from "util/types";
|
|
6574
|
-
|
|
6575
|
-
// src/config-storage/utf16.ts
|
|
6576
|
-
function hasLoneSurrogate(value) {
|
|
6577
|
-
for (let index = 0;index < value.length; index += 1) {
|
|
6578
|
-
const code = value.charCodeAt(index);
|
|
6579
|
-
if (code >= 55296 && code <= 56319) {
|
|
6580
|
-
if (index + 1 >= value.length)
|
|
6581
|
-
return true;
|
|
6582
|
-
const next = value.charCodeAt(index + 1);
|
|
6583
|
-
if (next < 56320 || next > 57343)
|
|
6584
|
-
return true;
|
|
6585
|
-
index += 1;
|
|
6586
|
-
} else if (code >= 56320 && code <= 57343) {
|
|
6587
|
-
return true;
|
|
6588
|
-
}
|
|
6589
|
-
}
|
|
6590
|
-
return false;
|
|
6591
|
-
}
|
|
6592
|
-
|
|
6593
|
-
// src/config-storage/json-preflight.ts
|
|
6594
|
-
var FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
6595
|
-
function childPath(parent, key) {
|
|
6596
|
-
return parent ? `${parent}.${key}` : key;
|
|
6597
|
-
}
|
|
6598
|
-
function symbolPath(parent, key) {
|
|
6599
|
-
const segment = `[Symbol(${key.description ?? ""})]`;
|
|
6600
|
-
return `${parent}${segment}`;
|
|
6601
|
-
}
|
|
6602
|
-
function addInvalid(context, path) {
|
|
6603
|
-
context.add("non_json_value", path, "Expected a JSON-compatible value");
|
|
6604
|
-
}
|
|
6605
|
-
function isCanonicalArrayIndex(key) {
|
|
6606
|
-
if (!/^(?:0|[1-9]\d*)$/.test(key))
|
|
6607
|
-
return false;
|
|
6608
|
-
const index = Number(key);
|
|
6609
|
-
return Number.isInteger(index) && index >= 0 && index <= 4294967294 && String(index) === key;
|
|
6610
|
-
}
|
|
6611
|
-
function inspectObject(pending, stack, seen, context) {
|
|
6612
|
-
const source = pending.value;
|
|
6613
|
-
let array;
|
|
6614
|
-
let prototype;
|
|
6615
|
-
let properties;
|
|
6616
|
-
let arrayLength = 0;
|
|
6617
|
-
try {
|
|
6618
|
-
if (isProxy(source))
|
|
6619
|
-
throw new TypeError("Proxy objects are not JSON values");
|
|
6620
|
-
if (seen.has(source)) {
|
|
6621
|
-
addInvalid(context, pending.path);
|
|
6622
|
-
return;
|
|
6623
|
-
}
|
|
6624
|
-
seen.add(source);
|
|
6625
|
-
array = Array.isArray(source);
|
|
6626
|
-
prototype = Object.getPrototypeOf(source);
|
|
6627
|
-
properties = Reflect.ownKeys(source).map((key) => {
|
|
6628
|
-
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
6629
|
-
if (!descriptor)
|
|
6630
|
-
throw new TypeError("Missing own property descriptor");
|
|
6631
|
-
return { key, descriptor };
|
|
6632
|
-
});
|
|
6633
|
-
if (array) {
|
|
6634
|
-
const descriptor = Object.getOwnPropertyDescriptor(source, "length");
|
|
6635
|
-
if (!descriptor || typeof descriptor.value !== "number") {
|
|
6636
|
-
throw new TypeError("Invalid array length descriptor");
|
|
6637
|
-
}
|
|
6638
|
-
arrayLength = descriptor.value;
|
|
6639
|
-
}
|
|
6640
|
-
if (Object.getPrototypeOf(source) !== prototype) {
|
|
6641
|
-
throw new TypeError("Prototype changed during reflection");
|
|
6642
|
-
}
|
|
6643
|
-
} catch {
|
|
6644
|
-
addInvalid(context, pending.path);
|
|
6645
|
-
return;
|
|
6646
|
-
}
|
|
6647
|
-
if (!array && prototype !== Object.prototype && prototype !== null || array && prototype !== Array.prototype) {
|
|
6648
|
-
addInvalid(context, pending.path);
|
|
6649
|
-
return;
|
|
6650
|
-
}
|
|
6651
|
-
const target = array ? [] : {};
|
|
6652
|
-
pending.assign(target);
|
|
6653
|
-
const children = [];
|
|
6654
|
-
const indexes = [];
|
|
6655
|
-
for (const { key, descriptor } of properties) {
|
|
6656
|
-
if (array && key === "length")
|
|
6657
|
-
continue;
|
|
6658
|
-
if (typeof key === "symbol") {
|
|
6659
|
-
addInvalid(context, symbolPath(pending.path, key));
|
|
6660
|
-
continue;
|
|
6661
|
-
}
|
|
6662
|
-
const propertyName = String(key);
|
|
6663
|
-
const arrayIndex = array && isCanonicalArrayIndex(propertyName);
|
|
6664
|
-
const path = arrayIndex ? `${pending.path}[${propertyName}]` : childPath(pending.path, propertyName);
|
|
6665
|
-
if (arrayIndex)
|
|
6666
|
-
indexes.push(Number(propertyName));
|
|
6667
|
-
if (array && !arrayIndex) {
|
|
6668
|
-
addInvalid(context, path);
|
|
6669
|
-
continue;
|
|
6670
|
-
}
|
|
6671
|
-
if (hasLoneSurrogate(propertyName))
|
|
6672
|
-
addInvalid(context, path);
|
|
6673
|
-
if (!descriptor.enumerable || !("value" in descriptor)) {
|
|
6674
|
-
addInvalid(context, path);
|
|
6675
|
-
continue;
|
|
6676
|
-
}
|
|
6677
|
-
if (FORBIDDEN_KEYS.has(propertyName)) {
|
|
6678
|
-
context.add("forbidden_key", path, "Key is forbidden in JSON configuration");
|
|
6679
|
-
children.push({ value: descriptor.value, path, assign() {} });
|
|
6680
|
-
continue;
|
|
6681
|
-
}
|
|
6682
|
-
children.push({
|
|
6683
|
-
value: descriptor.value,
|
|
6684
|
-
path,
|
|
6685
|
-
assign(value) {
|
|
6686
|
-
if (array)
|
|
6687
|
-
target[Number(propertyName)] = value;
|
|
6688
|
-
else
|
|
6689
|
-
Object.defineProperty(target, propertyName, {
|
|
6690
|
-
value,
|
|
6691
|
-
enumerable: true,
|
|
6692
|
-
writable: true,
|
|
6693
|
-
configurable: true
|
|
6694
|
-
});
|
|
6695
|
-
}
|
|
6696
|
-
});
|
|
6697
|
-
}
|
|
6698
|
-
if (array && indexes.length !== arrayLength) {
|
|
6699
|
-
indexes.sort((left, right) => left - right);
|
|
6700
|
-
let expected = 0;
|
|
6701
|
-
for (const index of indexes) {
|
|
6702
|
-
if (index !== expected)
|
|
6703
|
-
break;
|
|
6704
|
-
expected += 1;
|
|
6705
|
-
}
|
|
6706
|
-
addInvalid(context, `${pending.path}[${expected}]`);
|
|
6707
|
-
}
|
|
6708
|
-
for (let index = children.length - 1;index >= 0; index -= 1) {
|
|
6709
|
-
const child = children[index];
|
|
6710
|
-
if (child !== undefined)
|
|
6711
|
-
stack.push(child);
|
|
6712
|
-
}
|
|
6713
|
-
}
|
|
6714
|
-
|
|
6715
|
-
class JsonSnapshotError extends Error {
|
|
6716
|
-
errors;
|
|
6717
|
-
name = "JsonSnapshotError";
|
|
6718
|
-
constructor(errors) {
|
|
6719
|
-
super("Value is not a side-effect-free JSON graph");
|
|
6720
|
-
this.errors = errors;
|
|
6721
|
-
}
|
|
6722
|
-
}
|
|
6723
|
-
function snapshotJsonGraph(input) {
|
|
6724
|
-
const context = new ValidationContext;
|
|
6725
|
-
const value = preflightJsonGraph(input, context);
|
|
6726
|
-
if (value === undefined || context.errors.length > 0) {
|
|
6727
|
-
throw new JsonSnapshotError(context.errors);
|
|
6770
|
+
context.add(code, path, error.message);
|
|
6771
|
+
}
|
|
6728
6772
|
}
|
|
6729
|
-
|
|
6773
|
+
const logging = objectField(object.logging, "logging", ["body"], context);
|
|
6774
|
+
if (!logging || logging.body === undefined)
|
|
6775
|
+
return;
|
|
6776
|
+
const body = objectField(logging.body, "logging.body", ["enabled", "max_size", "retention_days"], context);
|
|
6777
|
+
if (!body)
|
|
6778
|
+
return;
|
|
6779
|
+
booleanField(body, "enabled", "logging.body", context);
|
|
6780
|
+
numberField(body, "max_size", "logging.body", context, { required: true, positive: true });
|
|
6781
|
+
numberField(body, "retention_days", "logging.body", context, { required: true, positive: true });
|
|
6730
6782
|
}
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
const seen = new WeakSet;
|
|
6737
|
-
while (stack.length) {
|
|
6738
|
-
const pending = stack.pop();
|
|
6739
|
-
if (pending === undefined)
|
|
6740
|
-
break;
|
|
6741
|
-
const value = pending.value;
|
|
6742
|
-
if (value === null || typeof value === "boolean")
|
|
6743
|
-
pending.assign(value);
|
|
6744
|
-
else if (typeof value === "string") {
|
|
6745
|
-
if (hasLoneSurrogate(value))
|
|
6746
|
-
addInvalid(context, pending.path);
|
|
6747
|
-
else
|
|
6748
|
-
pending.assign(value);
|
|
6749
|
-
} else if (typeof value === "number") {
|
|
6750
|
-
if (Number.isFinite(value))
|
|
6751
|
-
pending.assign(value);
|
|
6752
|
-
else
|
|
6753
|
-
addInvalid(context, pending.path);
|
|
6754
|
-
} else if (typeof value === "object")
|
|
6755
|
-
inspectObject(pending, stack, seen, context);
|
|
6756
|
-
else
|
|
6757
|
-
addInvalid(context, pending.path);
|
|
6758
|
-
}
|
|
6759
|
-
return context.errors.length ? undefined : output;
|
|
6783
|
+
|
|
6784
|
+
// src/config-storage/plugin-name.ts
|
|
6785
|
+
var PLUGIN_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
6786
|
+
function isPluginName(value) {
|
|
6787
|
+
return PLUGIN_NAME_PATTERN.test(value);
|
|
6760
6788
|
}
|
|
6761
6789
|
|
|
6762
6790
|
// src/plugin-manifest-catalog/manifest-json.ts
|
|
@@ -6958,7 +6986,9 @@ function modelMappings(value) {
|
|
|
6958
6986
|
if (!objectValue(item))
|
|
6959
6987
|
return false;
|
|
6960
6988
|
const keys = Object.keys(item);
|
|
6961
|
-
|
|
6989
|
+
const identity = item.catalogModel;
|
|
6990
|
+
const validIdentity = identity === undefined || objectValue(identity) && Object.keys(identity).length === 2 && Object.keys(identity).every((key) => ["provider", "model"].includes(key)) && ["provider", "model"].every((key) => typeof identity[key] === "string" && identity[key].length > 0 && identity[key] === identity[key].trim());
|
|
6991
|
+
return keys.every((key) => ["source", "target", "catalogModel"].includes(key)) && keys.includes("source") && keys.includes("target") && validIdentity && typeof item.source === "string" && item.source.length > 0 && item.source === item.source.trim() && typeof item.target === "string" && item.target.length > 0 && item.target === item.target.trim();
|
|
6962
6992
|
});
|
|
6963
6993
|
}
|
|
6964
6994
|
function validJsonString(value) {
|
|
@@ -7541,6 +7571,417 @@ function validateCommitCommand(command, compileOptions) {
|
|
|
7541
7571
|
return result.value;
|
|
7542
7572
|
}
|
|
7543
7573
|
|
|
7574
|
+
// src/config-storage/sqlite-query.ts
|
|
7575
|
+
function sqliteAll(db, sql, ...params) {
|
|
7576
|
+
const statement = db.prepare(sql);
|
|
7577
|
+
try {
|
|
7578
|
+
return statement.all(...params);
|
|
7579
|
+
} finally {
|
|
7580
|
+
statement.finalize();
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
function sqliteGet(db, sql, ...params) {
|
|
7584
|
+
const statement = db.prepare(sql);
|
|
7585
|
+
try {
|
|
7586
|
+
return statement.get(...params);
|
|
7587
|
+
} finally {
|
|
7588
|
+
statement.finalize();
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
7591
|
+
|
|
7592
|
+
// src/config-storage/retirement-store.ts
|
|
7593
|
+
function readPublicationRetirementRetry(db, requestId) {
|
|
7594
|
+
if (!isLowercaseUuid(requestId))
|
|
7595
|
+
invalid("retirement retry identity is invalid");
|
|
7596
|
+
const row = sqliteGet(db, "SELECT request_id,mutation_id,expected_revision FROM configuration_retirement_retry_requests WHERE request_id=?", requestId);
|
|
7597
|
+
return row ? { request_id: row.request_id, source_mutation_id: row.mutation_id, target_revision: row.expected_revision, status: "accepted" } : null;
|
|
7598
|
+
}
|
|
7599
|
+
function requestPublicationRetirementRetry(db, requestId, mutationId, revision, now) {
|
|
7600
|
+
if (!isLowercaseUuid(requestId) || !natural(revision, 1) || !natural(now))
|
|
7601
|
+
invalid("retirement retry request is invalid");
|
|
7602
|
+
const existing = readPublicationRetirementRetry(db, requestId);
|
|
7603
|
+
if (existing) {
|
|
7604
|
+
if (existing.source_mutation_id !== mutationId || existing.target_revision !== revision)
|
|
7605
|
+
throw new ConfigRepositoryError("cas_conflict", "retirement retry identity was reused");
|
|
7606
|
+
return existing;
|
|
7607
|
+
}
|
|
7608
|
+
const operation = sqliteGet(db, "SELECT state,committed_revision FROM configuration_operations WHERE mutation_id=?", mutationId);
|
|
7609
|
+
const retirement = readPublicationRetirement(db, mutationId);
|
|
7610
|
+
if (operation?.state !== "draining" || operation.committed_revision !== revision || retirement?.state !== "stopped")
|
|
7611
|
+
invalid("only stopped active draining cleanup may retry");
|
|
7612
|
+
db.run("INSERT INTO configuration_retirement_retry_requests(request_id,mutation_id,expected_revision,created_at) VALUES(?,?,?,?)", [requestId, mutationId, revision, now]);
|
|
7613
|
+
retryPublicationRetirement(db, mutationId, now);
|
|
7614
|
+
return readPublicationRetirementRetry(db, requestId);
|
|
7615
|
+
}
|
|
7616
|
+
function invalid(message) {
|
|
7617
|
+
throw new ConfigRepositoryError("invalid_operation", message);
|
|
7618
|
+
}
|
|
7619
|
+
function natural(value, minimum = 0) {
|
|
7620
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
7621
|
+
}
|
|
7622
|
+
function parseMembers(value) {
|
|
7623
|
+
const input = snapshotJsonGraph(value);
|
|
7624
|
+
if (!Array.isArray(input))
|
|
7625
|
+
invalid("retirement members must be an array");
|
|
7626
|
+
const seen = new Set;
|
|
7627
|
+
const result = input.map((raw) => {
|
|
7628
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
7629
|
+
invalid("retirement member is invalid");
|
|
7630
|
+
const item = raw;
|
|
7631
|
+
const identity = item.identity;
|
|
7632
|
+
if (!identity || !isLowercaseUuid(identity.master_generation) || !isLowercaseUuid(identity.worker_instance_id) || !natural(identity.worker_slot) || !isLowercaseUuid(item.boot_nonce) || !natural(item.pid, 1) || !natural(item.private_port, 1) || item.private_port > 65535 || !natural(item.revision, 1) || !validateDigest(item.content_hash) || !validateDigest(item.plugin_catalog_hash) || !isLowercaseUuid(item.drain_id) || typeof item.deadline_boot_id !== "string" || !item.deadline_boot_id.length || item.start_deadline_ns !== null && (typeof item.start_deadline_ns !== "string" || !/^[1-9]\d{0,24}$/.test(item.start_deadline_ns)))
|
|
7633
|
+
invalid("retirement member identity or deadline is invalid");
|
|
7634
|
+
if (seen.has(identity.worker_instance_id))
|
|
7635
|
+
invalid("retirement members contain duplicate identities");
|
|
7636
|
+
seen.add(identity.worker_instance_id);
|
|
7637
|
+
if (item.publication !== null && (!item.publication || typeof item.publication.mutation_id !== "string" || !natural(item.publication.attempt_no, 1) || !natural(item.publication.drain_recovery_generation)))
|
|
7638
|
+
invalid("retirement publication attempt is invalid");
|
|
7639
|
+
const guard = item.guard === null ? null : parseProcessLifetimeGuardFacts(item.guard);
|
|
7640
|
+
if (guard !== null && guard.process_instance_id !== identity.worker_instance_id)
|
|
7641
|
+
invalid("retirement guard belongs to another process");
|
|
7642
|
+
return Object.freeze({ ...item, identity: Object.freeze({ ...identity }), guard });
|
|
7643
|
+
});
|
|
7644
|
+
return Object.freeze(result);
|
|
7645
|
+
}
|
|
7646
|
+
function readPublicationRetirement(db, mutationId) {
|
|
7647
|
+
const row = sqliteGet(db, "SELECT generation,members_json,state,reason_code FROM configuration_retirements WHERE mutation_id=?", mutationId);
|
|
7648
|
+
if (row === null)
|
|
7649
|
+
return null;
|
|
7650
|
+
const members = parseMembers(JSON.parse(row.members_json));
|
|
7651
|
+
const evidence = sqliteAll(db, "SELECT member_id,exit_state,cleanup_state,exit_boot_id,exit_deadline_ns,start_boot_id,start_deadline_ns FROM configuration_retirement_evidence WHERE mutation_id=? ORDER BY member_id", mutationId);
|
|
7652
|
+
if (evidence.length !== members.length || evidence.some((item) => !members.some((member) => member.identity.worker_instance_id === item.member_id))) {
|
|
7653
|
+
throw new ConfigRepositoryError("schema_corrupt", "retirement evidence does not cover the frozen members");
|
|
7654
|
+
}
|
|
7655
|
+
return Object.freeze({
|
|
7656
|
+
mutation_id: mutationId,
|
|
7657
|
+
generation: row.generation,
|
|
7658
|
+
members,
|
|
7659
|
+
state: row.state,
|
|
7660
|
+
reason_code: row.reason_code,
|
|
7661
|
+
evidence: Object.freeze(evidence.map((item) => Object.freeze(item)))
|
|
7662
|
+
});
|
|
7663
|
+
}
|
|
7664
|
+
function freezePublicationRetirement(db, mutationId, generation, value, now) {
|
|
7665
|
+
if (!natural(generation) || !natural(now))
|
|
7666
|
+
invalid("retirement generation or timestamp is invalid");
|
|
7667
|
+
const members = parseMembers(value);
|
|
7668
|
+
const prior = readPublicationRetirement(db, mutationId);
|
|
7669
|
+
if (prior !== null) {
|
|
7670
|
+
if (prior.generation !== generation || canonicalJson(prior.members) !== canonicalJson(members)) {
|
|
7671
|
+
throw new ConfigRepositoryError("cas_conflict", "retirement membership is already frozen");
|
|
7672
|
+
}
|
|
7673
|
+
return prior;
|
|
7674
|
+
}
|
|
7675
|
+
const operation = sqliteGet(db, "SELECT state,drain_recovery_generation FROM configuration_operations WHERE mutation_id=?", mutationId);
|
|
7676
|
+
if (operation === null || operation.state === "converged" || operation.state === "degraded" || operation.drain_recovery_generation !== generation)
|
|
7677
|
+
invalid("retirement must match the active publication generation");
|
|
7678
|
+
db.run("INSERT INTO configuration_retirements(mutation_id,generation,members_json,state,updated_at) VALUES(?,?,?,'pending',?)", [mutationId, generation, canonicalJson(members), now]);
|
|
7679
|
+
for (const member of members)
|
|
7680
|
+
db.run(`INSERT INTO configuration_retirement_evidence(mutation_id,member_id,exit_state,cleanup_state,updated_at)
|
|
7681
|
+
VALUES(?,?,'unknown','unknown',?)`, [mutationId, member.identity.worker_instance_id, now]);
|
|
7682
|
+
return readPublicationRetirement(db, mutationId);
|
|
7683
|
+
}
|
|
7684
|
+
function freezePublicationRetirementDrainStart(db, mutationId, memberId, bootId, deadlineNs, now) {
|
|
7685
|
+
const before = readPublicationRetirement(db, mutationId);
|
|
7686
|
+
const member = before?.members.find((item) => item.identity.worker_instance_id === memberId);
|
|
7687
|
+
if (!before || !member || member.deadline_boot_id !== bootId || !natural(now) || !/^[1-9]\d{0,24}$/.test(deadlineNs))
|
|
7688
|
+
invalid("retirement drain start is invalid");
|
|
7689
|
+
const saved = before.evidence.find((item) => item.member_id === memberId);
|
|
7690
|
+
if (saved.start_deadline_ns !== null)
|
|
7691
|
+
return before;
|
|
7692
|
+
const deadline = member.start_deadline_ns ?? deadlineNs;
|
|
7693
|
+
db.run("UPDATE configuration_retirement_evidence SET start_boot_id=?,start_deadline_ns=?,updated_at=? WHERE mutation_id=? AND member_id=?", [bootId, deadline, now, mutationId, memberId]);
|
|
7694
|
+
return readPublicationRetirement(db, mutationId);
|
|
7695
|
+
}
|
|
7696
|
+
function recordPublicationRetirementEvidence(db, mutationId, memberId, exit, cleanup, now, deadline) {
|
|
7697
|
+
if (!["live", "confirmed", "unknown"].includes(exit) || !["success", "failed", "unknown"].includes(cleanup) || !natural(now))
|
|
7698
|
+
invalid("retirement evidence is invalid");
|
|
7699
|
+
const before = readPublicationRetirement(db, mutationId);
|
|
7700
|
+
const evidence = before?.evidence.find((item) => item.member_id === memberId);
|
|
7701
|
+
if (!before || !evidence)
|
|
7702
|
+
invalid("retirement member is not part of the frozen authority");
|
|
7703
|
+
if (deadline !== undefined && (deadline.boot_id !== before.members.find((member) => member.identity.worker_instance_id === memberId).deadline_boot_id || !/^[1-9]\d{0,24}$/.test(deadline.exit_deadline_ns)))
|
|
7704
|
+
invalid("retirement terminal exit deadline is invalid");
|
|
7705
|
+
if (evidence.exit_deadline_ns !== null && deadline !== undefined && (evidence.exit_boot_id !== deadline.boot_id || evidence.exit_deadline_ns !== deadline.exit_deadline_ns))
|
|
7706
|
+
invalid("retirement terminal exit deadline is already frozen");
|
|
7707
|
+
if (evidence.cleanup_state !== "unknown") {
|
|
7708
|
+
if (cleanup !== "unknown" && cleanup !== evidence.cleanup_state)
|
|
7709
|
+
invalid("terminal retirement cleanup cannot be changed");
|
|
7710
|
+
cleanup = evidence.cleanup_state;
|
|
7711
|
+
}
|
|
7712
|
+
if (before.state === "released") {
|
|
7713
|
+
if (evidence.exit_state === exit && evidence.cleanup_state === cleanup)
|
|
7714
|
+
return before;
|
|
7715
|
+
throw new ConfigRepositoryError("cas_conflict", "released retirement is immutable");
|
|
7716
|
+
}
|
|
7717
|
+
if (evidence.exit_state === "confirmed" && exit !== "confirmed")
|
|
7718
|
+
invalid("confirmed retirement exit cannot be revoked");
|
|
7719
|
+
db.run("UPDATE configuration_retirement_evidence SET exit_state=?,cleanup_state=?,exit_boot_id=?,exit_deadline_ns=?,updated_at=? WHERE mutation_id=? AND member_id=?", [exit, cleanup, evidence.exit_boot_id ?? deadline?.boot_id ?? null, evidence.exit_deadline_ns ?? deadline?.exit_deadline_ns ?? null, now, mutationId, memberId]);
|
|
7720
|
+
return readPublicationRetirement(db, mutationId);
|
|
7721
|
+
}
|
|
7722
|
+
function stopPublicationRetirement(db, mutationId, reason, now) {
|
|
7723
|
+
if (!["exit_unknown", "cleanup_unknown", "release_unknown"].includes(reason) || !natural(now))
|
|
7724
|
+
invalid("retirement stop reason is invalid");
|
|
7725
|
+
const before = readPublicationRetirement(db, mutationId);
|
|
7726
|
+
if (!before)
|
|
7727
|
+
invalid("retirement is not frozen");
|
|
7728
|
+
if (before.state === "released")
|
|
7729
|
+
return before;
|
|
7730
|
+
db.run("UPDATE configuration_retirements SET state='stopped',reason_code=?,updated_at=? WHERE mutation_id=?", [reason, now, mutationId]);
|
|
7731
|
+
return readPublicationRetirement(db, mutationId);
|
|
7732
|
+
}
|
|
7733
|
+
function retryPublicationRetirement(db, mutationId, now) {
|
|
7734
|
+
const before = readPublicationRetirement(db, mutationId);
|
|
7735
|
+
if (!before || !natural(now))
|
|
7736
|
+
invalid("retirement retry is invalid");
|
|
7737
|
+
if (before.state === "released" || before.state === "pending")
|
|
7738
|
+
return before;
|
|
7739
|
+
db.run("UPDATE configuration_retirements SET state='pending',reason_code=NULL,updated_at=? WHERE mutation_id=?", [now, mutationId]);
|
|
7740
|
+
return readPublicationRetirement(db, mutationId);
|
|
7741
|
+
}
|
|
7742
|
+
function markPublicationRetirementReleased(db, mutationId, now) {
|
|
7743
|
+
const before = readPublicationRetirement(db, mutationId);
|
|
7744
|
+
if (!before || !natural(now))
|
|
7745
|
+
invalid("retirement release is invalid");
|
|
7746
|
+
if (before.state === "released")
|
|
7747
|
+
return before;
|
|
7748
|
+
if (before.evidence.some((item) => item.exit_state !== "confirmed"))
|
|
7749
|
+
invalid("retirement release requires every durable exit fence");
|
|
7750
|
+
db.run("UPDATE configuration_retirements SET state='released',reason_code=NULL,updated_at=? WHERE mutation_id=?", [now, mutationId]);
|
|
7751
|
+
return readPublicationRetirement(db, mutationId);
|
|
7752
|
+
}
|
|
7753
|
+
function readRetiredWorkerInstanceIds(db) {
|
|
7754
|
+
const rows = sqliteAll(db, "SELECT member_id FROM configuration_retirement_evidence WHERE exit_state='confirmed' UNION SELECT value AS member_id FROM configuration_publication_serving_sets,json_each(exited_json) ORDER BY member_id");
|
|
7755
|
+
if (rows.some((row) => !isLowercaseUuid(row.member_id)))
|
|
7756
|
+
throw new ConfigRepositoryError("schema_corrupt", "retired process identity is invalid");
|
|
7757
|
+
return Object.freeze(rows.map((row) => row.member_id));
|
|
7758
|
+
}
|
|
7759
|
+
function readRetirementDescriptorCleanupState(db, workerInstanceId) {
|
|
7760
|
+
if (!isLowercaseUuid(workerInstanceId))
|
|
7761
|
+
invalid("retirement descriptor identity is invalid");
|
|
7762
|
+
const rows = sqliteAll(db, `SELECT o.state FROM configuration_retirement_evidence e
|
|
7763
|
+
JOIN configuration_operations o USING(mutation_id) WHERE e.member_id=?`, workerInstanceId);
|
|
7764
|
+
if (!rows.length)
|
|
7765
|
+
return "absent";
|
|
7766
|
+
return rows.every((row) => row.state === "converged" || row.state === "degraded") ? "terminal" : "pending";
|
|
7767
|
+
}
|
|
7768
|
+
function parseServingMembers(value) {
|
|
7769
|
+
const input = snapshotJsonGraph(value);
|
|
7770
|
+
if (!Array.isArray(input))
|
|
7771
|
+
invalid("publication serving members are invalid");
|
|
7772
|
+
const identities = new Set;
|
|
7773
|
+
return Object.freeze(input.map((raw) => {
|
|
7774
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
7775
|
+
invalid("publication serving member is invalid");
|
|
7776
|
+
const item = raw;
|
|
7777
|
+
const identity = item.identity;
|
|
7778
|
+
if (!identity || !isLowercaseUuid(identity.master_generation) || !isLowercaseUuid(identity.worker_instance_id) || !natural(identity.worker_slot) || !isLowercaseUuid(item.boot_nonce) || !natural(item.pid, 1) || !natural(item.private_port, 1) || item.private_port > 65535 || !natural(item.revision, 1) || !validateDigest(item.content_hash) || !validateDigest(item.plugin_catalog_hash) || item.publication === null || !item.publication || typeof item.publication.mutation_id !== "string" || !natural(item.publication.attempt_no, 1) || !natural(item.publication.drain_recovery_generation) || typeof item.kernel_boot_id !== "string" || !item.kernel_boot_id.length || identities.has(identity.worker_instance_id))
|
|
7779
|
+
invalid("publication serving member identity or attempt is invalid");
|
|
7780
|
+
identities.add(identity.worker_instance_id);
|
|
7781
|
+
const guard = item.guard === null ? null : parseProcessLifetimeGuardFacts(item.guard);
|
|
7782
|
+
if (guard !== null && guard.process_instance_id !== identity.worker_instance_id)
|
|
7783
|
+
invalid("publication serving guard identity is invalid");
|
|
7784
|
+
return Object.freeze({ ...item, identity: Object.freeze({ ...identity }), guard });
|
|
7785
|
+
}));
|
|
7786
|
+
}
|
|
7787
|
+
function readPublicationServingSet(db, mutationId, generation) {
|
|
7788
|
+
if (!natural(generation))
|
|
7789
|
+
invalid("publication serving generation is invalid");
|
|
7790
|
+
const attemptKey = currentServingAttemptKey(db, mutationId);
|
|
7791
|
+
const row = sqliteGet(db, "SELECT workers_json,exited_json FROM configuration_publication_serving_sets WHERE mutation_id=? AND generation=? AND attempt_key=?", mutationId, generation, attemptKey);
|
|
7792
|
+
if (row === null)
|
|
7793
|
+
return null;
|
|
7794
|
+
const workers = parseServingMembers(JSON.parse(row.workers_json));
|
|
7795
|
+
if (canonicalJson(workers.map((worker) => [worker.identity.worker_slot, worker.publication.attempt_no]).sort((a, b) => a[0] - b[0])) !== attemptKey || workers.some((worker) => worker.publication.mutation_id !== mutationId || worker.publication.drain_recovery_generation !== generation)) {
|
|
7796
|
+
throw new ConfigRepositoryError("schema_corrupt", "publication serving attempt identity is invalid");
|
|
7797
|
+
}
|
|
7798
|
+
const exited = JSON.parse(row.exited_json);
|
|
7799
|
+
if (!Array.isArray(exited) || new Set(exited).size !== exited.length || exited.some((id) => typeof id !== "string" || !workers.some((worker) => worker.identity.worker_instance_id === id))) {
|
|
7800
|
+
throw new ConfigRepositoryError("schema_corrupt", "publication serving exit fence is invalid");
|
|
7801
|
+
}
|
|
7802
|
+
return Object.freeze({ mutation_id: mutationId, generation, attempt_key: attemptKey, workers, exited: Object.freeze(exited) });
|
|
7803
|
+
}
|
|
7804
|
+
function currentServingAttemptKey(db, mutationId) {
|
|
7805
|
+
const targets = sqliteAll(db, "SELECT worker_slot,attempt_no FROM configuration_operation_workers WHERE mutation_id=? ORDER BY worker_slot", mutationId);
|
|
7806
|
+
return canonicalJson(targets.map((target) => [target.worker_slot, target.attempt_no]));
|
|
7807
|
+
}
|
|
7808
|
+
function assertPublicationServingAttemptsExited(db, mutationId) {
|
|
7809
|
+
const history = sqliteAll(db, "SELECT workers_json,exited_json FROM configuration_publication_serving_sets WHERE mutation_id=?", mutationId);
|
|
7810
|
+
for (const prior of history) {
|
|
7811
|
+
const exited = JSON.parse(prior.exited_json);
|
|
7812
|
+
const workers = parseServingMembers(JSON.parse(prior.workers_json));
|
|
7813
|
+
if (!Array.isArray(exited) || exited.some((id) => typeof id !== "string" || !workers.some((worker) => worker.identity.worker_instance_id === id))) {
|
|
7814
|
+
throw new ConfigRepositoryError("schema_corrupt", "publication serving exit fence is invalid");
|
|
7815
|
+
}
|
|
7816
|
+
if (workers.some((worker) => !exited.includes(worker.identity.worker_instance_id))) {
|
|
7817
|
+
invalid("a prior serving attempt has no complete durable exit fence");
|
|
7818
|
+
}
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
function freezePublicationServingSet(db, mutationId, generation, input) {
|
|
7822
|
+
const workers = parseServingMembers(input);
|
|
7823
|
+
const before = readPublicationServingSet(db, mutationId, generation);
|
|
7824
|
+
if (before !== null) {
|
|
7825
|
+
if (canonicalJson(before.workers) !== canonicalJson(workers))
|
|
7826
|
+
throw new ConfigRepositoryError("cas_conflict", "publication serving members are already frozen");
|
|
7827
|
+
return before;
|
|
7828
|
+
}
|
|
7829
|
+
const operation = sqliteGet(db, "SELECT state,committed_revision,target_worker_count,drain_recovery_generation FROM configuration_operations WHERE mutation_id=?", mutationId);
|
|
7830
|
+
if (!operation || operation.state === "converged" || operation.state === "degraded" || operation.drain_recovery_generation !== generation || workers.length !== operation.target_worker_count || workers.some((worker) => worker.revision !== operation.committed_revision || worker.publication?.mutation_id !== mutationId || worker.publication.drain_recovery_generation !== generation))
|
|
7831
|
+
invalid("publication serving authority does not match the current attempt");
|
|
7832
|
+
const attemptKey = currentServingAttemptKey(db, mutationId);
|
|
7833
|
+
if (canonicalJson(workers.map((worker) => [worker.identity.worker_slot, worker.publication.attempt_no]).sort((a, b) => a[0] - b[0])) !== attemptKey) {
|
|
7834
|
+
invalid("publication serving authority does not cover every target attempt");
|
|
7835
|
+
}
|
|
7836
|
+
for (const worker of workers) {
|
|
7837
|
+
const target = sqliteGet(db, "SELECT attempt_no,state FROM configuration_operation_workers WHERE mutation_id=? AND worker_slot=?", mutationId, worker.identity.worker_slot);
|
|
7838
|
+
if (target === null || target.state !== "converged" || target.attempt_no !== worker.publication?.attempt_no)
|
|
7839
|
+
invalid("publication serving attempt is not converged");
|
|
7840
|
+
}
|
|
7841
|
+
assertPublicationServingAttemptsExited(db, mutationId);
|
|
7842
|
+
db.run("INSERT INTO configuration_publication_serving_sets(mutation_id,generation,attempt_key,workers_json,exited_json) VALUES(?,?,?,?,'[]')", [mutationId, generation, attemptKey, canonicalJson(workers)]);
|
|
7843
|
+
return readPublicationServingSet(db, mutationId, generation);
|
|
7844
|
+
}
|
|
7845
|
+
function confirmPublicationServingMemberExit(db, mutationId, generation, workerInstanceId) {
|
|
7846
|
+
const before = readPublicationServingSet(db, mutationId, generation);
|
|
7847
|
+
if (!before || !before.workers.some((worker) => worker.identity.worker_instance_id === workerInstanceId))
|
|
7848
|
+
invalid("publication serving member is outside durable authority");
|
|
7849
|
+
if (before.exited.includes(workerInstanceId))
|
|
7850
|
+
return before;
|
|
7851
|
+
db.run("UPDATE configuration_publication_serving_sets SET exited_json=? WHERE mutation_id=? AND generation=? AND attempt_key=?", [canonicalJson([...before.exited, workerInstanceId].sort()), mutationId, generation, before.attempt_key]);
|
|
7852
|
+
return readPublicationServingSet(db, mutationId, generation);
|
|
7853
|
+
}
|
|
7854
|
+
|
|
7855
|
+
// src/config-storage/runtime-recovery-budget.ts
|
|
7856
|
+
var BACKOFF_NS = [5000000000n, 30000000000n];
|
|
7857
|
+
var HEALTHY_RESET_NS = 30n * 60n * 1000000000n;
|
|
7858
|
+
var MAX_HEALTH_SAMPLE_GAP_NS = 60n * 1000000000n;
|
|
7859
|
+
function sample(input) {
|
|
7860
|
+
if (!isLowercaseUuid(input.owner) || typeof input.boot_id !== "string" || input.boot_id.length < 1 || input.boot_id.length > 256 || typeof input.monotonic_ns !== "string" || !/^(0|[1-9]\d{0,24})$/.test(input.monotonic_ns)) {
|
|
7861
|
+
throw new ConfigRepositoryError("invalid_operation", "runtime recovery sample is invalid");
|
|
7862
|
+
}
|
|
7863
|
+
return BigInt(input.monotonic_ns);
|
|
7864
|
+
}
|
|
7865
|
+
function readRuntimeRecoveryBudget(db) {
|
|
7866
|
+
const row = sqliteGet(db, "SELECT tickets,ticket_id,ticket_boot_id,retry_at_ns,healthy_owner,healthy_boot_id,healthy_since_ns,healthy_last_ns FROM runtime_recovery_budget WHERE id=1");
|
|
7867
|
+
if (row === null || !Number.isSafeInteger(row.tickets) || row.tickets < 0 || row.tickets > 2 || row.tickets > 0 && (row.ticket_id === null || !isLowercaseUuid(row.ticket_id) || typeof row.ticket_boot_id !== "string" || !row.ticket_boot_id.length || row.retry_at_ns === null) || row.healthy_owner !== null && (!isLowercaseUuid(row.healthy_owner) || typeof row.healthy_boot_id !== "string" || !row.healthy_boot_id.length || row.healthy_since_ns === null || row.healthy_last_ns === null) || [row.retry_at_ns, row.healthy_since_ns, row.healthy_last_ns].some((value) => value !== null && !/^(0|[1-9]\d{0,24})$/.test(value))) {
|
|
7868
|
+
throw new ConfigRepositoryError("schema_corrupt", "runtime recovery budget is corrupt");
|
|
7869
|
+
}
|
|
7870
|
+
return Object.freeze({
|
|
7871
|
+
tickets: row.tickets,
|
|
7872
|
+
last_ticket_id: row.ticket_id,
|
|
7873
|
+
retry_boot_id: row.ticket_boot_id,
|
|
7874
|
+
retry_at_ns: row.retry_at_ns,
|
|
7875
|
+
healthy_owner: row.healthy_owner,
|
|
7876
|
+
healthy_boot_id: row.healthy_boot_id,
|
|
7877
|
+
healthy_since_ns: row.healthy_since_ns,
|
|
7878
|
+
healthy_last_ns: row.healthy_last_ns
|
|
7879
|
+
});
|
|
7880
|
+
}
|
|
7881
|
+
function consumeRuntimeRecoveryRestartTicket(db, ticketId, input) {
|
|
7882
|
+
const now = sample(input);
|
|
7883
|
+
if (!isLowercaseUuid(ticketId))
|
|
7884
|
+
throw new ConfigRepositoryError("invalid_operation", "runtime recovery ticket identity is invalid");
|
|
7885
|
+
const before = readRuntimeRecoveryBudget(db);
|
|
7886
|
+
const prior = sqliteGet(db, "SELECT owner,boot_id FROM runtime_recovery_tickets WHERE ticket_id=?", ticketId);
|
|
7887
|
+
if (prior !== null) {
|
|
7888
|
+
if (prior.owner !== input.owner || prior.boot_id !== input.boot_id) {
|
|
7889
|
+
throw new ConfigRepositoryError("invalid_operation", "runtime recovery ticket belongs to another owner or kernel boot");
|
|
7890
|
+
}
|
|
7891
|
+
return before;
|
|
7892
|
+
}
|
|
7893
|
+
if (before.tickets >= 2)
|
|
7894
|
+
throw new ConfigRepositoryError("attempts_exhausted", "runtime recovery restart budget is exhausted");
|
|
7895
|
+
if (before.retry_at_ns !== null && before.retry_boot_id === input.boot_id && now < BigInt(before.retry_at_ns)) {
|
|
7896
|
+
throw new ConfigRepositoryError("retry_not_due", "runtime recovery restart backoff has not elapsed");
|
|
7897
|
+
}
|
|
7898
|
+
const retry = now + BACKOFF_NS[before.tickets];
|
|
7899
|
+
db.run("INSERT INTO runtime_recovery_tickets(ticket_id,owner,boot_id,consumed_at_ns,retry_at_ns) VALUES(?,?,?,?,?)", [ticketId, input.owner, input.boot_id, now.toString(), retry.toString()]);
|
|
7900
|
+
db.run(`UPDATE runtime_recovery_budget SET tickets=tickets+1,ticket_id=?,ticket_boot_id=?,retry_at_ns=?,
|
|
7901
|
+
healthy_owner=NULL,healthy_boot_id=NULL,healthy_since_ns=NULL,healthy_last_ns=NULL WHERE id=1`, [ticketId, input.boot_id, retry.toString()]);
|
|
7902
|
+
return readRuntimeRecoveryBudget(db);
|
|
7903
|
+
}
|
|
7904
|
+
function readRuntimeRecoveryStartupGate(db, input) {
|
|
7905
|
+
const now = sample(input);
|
|
7906
|
+
const budget = readRuntimeRecoveryBudget(db);
|
|
7907
|
+
if (budget.retry_at_ns === null)
|
|
7908
|
+
return { kind: "ready", budget };
|
|
7909
|
+
if (budget.retry_boot_id !== input.boot_id)
|
|
7910
|
+
return { kind: "stopped", reason: "clock_domain_unknown", budget };
|
|
7911
|
+
const remaining = BigInt(budget.retry_at_ns) - now;
|
|
7912
|
+
if (remaining > 0n)
|
|
7913
|
+
return { kind: "waiting", remaining_ms: Number((remaining + 999999n) / 1000000n), budget };
|
|
7914
|
+
return { kind: "ready", budget };
|
|
7915
|
+
}
|
|
7916
|
+
function observeRuntimeRecoveryHealth(db, input, healthy) {
|
|
7917
|
+
const now = sample(input);
|
|
7918
|
+
const before = readRuntimeRecoveryBudget(db);
|
|
7919
|
+
if (typeof healthy !== "boolean")
|
|
7920
|
+
throw new ConfigRepositoryError("invalid_operation", "runtime health sample is invalid");
|
|
7921
|
+
if (!healthy) {
|
|
7922
|
+
db.run("UPDATE runtime_recovery_budget SET healthy_owner=NULL,healthy_boot_id=NULL,healthy_since_ns=NULL,healthy_last_ns=NULL WHERE id=1");
|
|
7923
|
+
return readRuntimeRecoveryBudget(db);
|
|
7924
|
+
}
|
|
7925
|
+
const continuous = before.healthy_owner === input.owner && before.healthy_boot_id === input.boot_id && before.healthy_since_ns !== null && before.healthy_last_ns !== null && now >= BigInt(before.healthy_last_ns) && now - BigInt(before.healthy_last_ns) <= MAX_HEALTH_SAMPLE_GAP_NS;
|
|
7926
|
+
const since = continuous ? BigInt(before.healthy_since_ns) : now;
|
|
7927
|
+
if (now - since >= HEALTHY_RESET_NS) {
|
|
7928
|
+
db.run(`UPDATE runtime_recovery_budget SET tickets=0,ticket_id=NULL,ticket_boot_id=NULL,retry_at_ns=NULL,
|
|
7929
|
+
healthy_owner=?,healthy_boot_id=?,healthy_since_ns=?,healthy_last_ns=? WHERE id=1`, [input.owner, input.boot_id, since.toString(), now.toString()]);
|
|
7930
|
+
} else {
|
|
7931
|
+
db.run("UPDATE runtime_recovery_budget SET healthy_owner=?,healthy_boot_id=?,healthy_since_ns=?,healthy_last_ns=? WHERE id=1", [input.owner, input.boot_id, since.toString(), now.toString()]);
|
|
7932
|
+
}
|
|
7933
|
+
return readRuntimeRecoveryBudget(db);
|
|
7934
|
+
}
|
|
7935
|
+
|
|
7936
|
+
// src/config-storage/config-repository.ts
|
|
7937
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
7938
|
+
import { mkdirSync } from "fs";
|
|
7939
|
+
import { dirname as dirname2 } from "path";
|
|
7940
|
+
|
|
7941
|
+
// src/config-storage/persisted-validation.ts
|
|
7942
|
+
function corrupt(field) {
|
|
7943
|
+
throw new ConfigRepositoryError("schema_corrupt", `${field} contains an invalid persisted value`);
|
|
7944
|
+
}
|
|
7945
|
+
function requireSafeInteger(value, field, minimum = 0) {
|
|
7946
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
7947
|
+
return corrupt(field);
|
|
7948
|
+
return value;
|
|
7949
|
+
}
|
|
7950
|
+
function requireBooleanInteger(value, field) {
|
|
7951
|
+
if (value !== 0 && value !== 1)
|
|
7952
|
+
return corrupt(field);
|
|
7953
|
+
return value === 1;
|
|
7954
|
+
}
|
|
7955
|
+
function requirePositiveFinite(value, field) {
|
|
7956
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
7957
|
+
return corrupt(field);
|
|
7958
|
+
return value;
|
|
7959
|
+
}
|
|
7960
|
+
function requireUuid(value, field) {
|
|
7961
|
+
if (!isLowercaseUuid(value))
|
|
7962
|
+
return corrupt(field);
|
|
7963
|
+
return value;
|
|
7964
|
+
}
|
|
7965
|
+
function requirePluginName(value, field) {
|
|
7966
|
+
if (!isPluginName(value))
|
|
7967
|
+
return corrupt(field);
|
|
7968
|
+
return value;
|
|
7969
|
+
}
|
|
7970
|
+
function requireOptionalGlobalScalar(value, field) {
|
|
7971
|
+
if (value === null)
|
|
7972
|
+
return;
|
|
7973
|
+
const valid = field === "log_level" ? isLogLevel(value) : isBodyParserLimit(value);
|
|
7974
|
+
if (!valid)
|
|
7975
|
+
return corrupt(field);
|
|
7976
|
+
return value;
|
|
7977
|
+
}
|
|
7978
|
+
function requireOneOf(value, allowed, field) {
|
|
7979
|
+
const match = allowed.find((candidate) => candidate === value);
|
|
7980
|
+
if (match === undefined)
|
|
7981
|
+
return corrupt(field);
|
|
7982
|
+
return match;
|
|
7983
|
+
}
|
|
7984
|
+
|
|
7544
7985
|
// src/config-storage/operation-records.ts
|
|
7545
7986
|
var OPERATION_SELECT = `SELECT mutation_id,request_hash,expected_revision,committed_revision,kind,state,
|
|
7546
7987
|
target_worker_count,result_status,error_code,error_detail,drain_recovery_generation,
|
|
@@ -7890,84 +8331,6 @@ function readActiveRevision(db) {
|
|
|
7890
8331
|
return revision;
|
|
7891
8332
|
}
|
|
7892
8333
|
|
|
7893
|
-
// ../../node_modules/canonicalize/lib/canonicalize.js
|
|
7894
|
-
function canonicalize(object, seen = new Set) {
|
|
7895
|
-
if (typeof object === "number" && isNaN(object)) {
|
|
7896
|
-
throw new Error("NaN is not allowed");
|
|
7897
|
-
}
|
|
7898
|
-
if (typeof object === "number" && !isFinite(object)) {
|
|
7899
|
-
throw new Error("Infinity is not allowed");
|
|
7900
|
-
}
|
|
7901
|
-
if (object === null || typeof object !== "object") {
|
|
7902
|
-
return JSON.stringify(object);
|
|
7903
|
-
}
|
|
7904
|
-
if (typeof object.toJSON === "function") {
|
|
7905
|
-
if (seen.has(object)) {
|
|
7906
|
-
throw new Error("Circular reference detected");
|
|
7907
|
-
}
|
|
7908
|
-
seen.add(object);
|
|
7909
|
-
const result = canonicalize(object.toJSON(), seen);
|
|
7910
|
-
seen.delete(object);
|
|
7911
|
-
return result;
|
|
7912
|
-
}
|
|
7913
|
-
if (seen.has(object)) {
|
|
7914
|
-
throw new Error("Circular reference detected");
|
|
7915
|
-
}
|
|
7916
|
-
seen.add(object);
|
|
7917
|
-
let result;
|
|
7918
|
-
if (Array.isArray(object)) {
|
|
7919
|
-
const values = object.map((cv) => {
|
|
7920
|
-
const value = cv === undefined || typeof cv === "symbol" ? null : cv;
|
|
7921
|
-
return canonicalize(value, seen);
|
|
7922
|
-
});
|
|
7923
|
-
result = `[${values.join(",")}]`;
|
|
7924
|
-
} else {
|
|
7925
|
-
const parts = [];
|
|
7926
|
-
for (const key of Object.keys(object).sort()) {
|
|
7927
|
-
if (object[key] === undefined || typeof object[key] === "symbol") {
|
|
7928
|
-
continue;
|
|
7929
|
-
}
|
|
7930
|
-
parts.push(`${canonicalize(key)}:${canonicalize(object[key], seen)}`);
|
|
7931
|
-
}
|
|
7932
|
-
result = `{${parts.join(",")}}`;
|
|
7933
|
-
}
|
|
7934
|
-
seen.delete(object);
|
|
7935
|
-
return result;
|
|
7936
|
-
}
|
|
7937
|
-
|
|
7938
|
-
// src/config-storage/content-hash.ts
|
|
7939
|
-
class ConfigurationHashError extends Error {
|
|
7940
|
-
cause;
|
|
7941
|
-
name = "ConfigurationHashError";
|
|
7942
|
-
constructor(cause) {
|
|
7943
|
-
super("RFC 8785 canonicalization failed", { cause });
|
|
7944
|
-
this.cause = cause;
|
|
7945
|
-
}
|
|
7946
|
-
}
|
|
7947
|
-
function canonicalJson(value) {
|
|
7948
|
-
try {
|
|
7949
|
-
const result = canonicalize(snapshotJsonGraph(value));
|
|
7950
|
-
if (result === undefined)
|
|
7951
|
-
throw new TypeError("canonicalize returned undefined");
|
|
7952
|
-
return result;
|
|
7953
|
-
} catch (error) {
|
|
7954
|
-
throw new ConfigurationHashError(error);
|
|
7955
|
-
}
|
|
7956
|
-
}
|
|
7957
|
-
function hashConfigurationContent(value) {
|
|
7958
|
-
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
7959
|
-
const hex = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
7960
|
-
return `sha256:${hex}`;
|
|
7961
|
-
}
|
|
7962
|
-
function hashConfigurationRequest(identity) {
|
|
7963
|
-
return hashConfigurationContent({
|
|
7964
|
-
kind: identity.kind,
|
|
7965
|
-
expected_revision: identity.expected_revision,
|
|
7966
|
-
aggregate: identity.aggregate,
|
|
7967
|
-
target_worker_slots: [...identity.target_worker_slots].sort((left, right) => left - right)
|
|
7968
|
-
});
|
|
7969
|
-
}
|
|
7970
|
-
|
|
7971
8334
|
// src/config-storage/operation-relations.ts
|
|
7972
8335
|
function validateActiveOperationIdentity(db, operation, workers, snapshot) {
|
|
7973
8336
|
validateOperationRelations(operation, workers);
|
|
@@ -8195,7 +8558,7 @@ var REASON_CODES = [
|
|
|
8195
8558
|
var RECOVERY_SELECT = `SELECT recovery_sequence,recovery_id,source_mutation_id,target_revision,trigger,state,
|
|
8196
8559
|
attempt_count,max_attempts,next_retry_at,final_reason_code,final_reason_detail,created_at,updated_at
|
|
8197
8560
|
FROM configuration_recoveries`;
|
|
8198
|
-
function
|
|
8561
|
+
function invalid2(message) {
|
|
8199
8562
|
throw new ConfigRepositoryError("invalid_operation", message);
|
|
8200
8563
|
}
|
|
8201
8564
|
function corrupt2(message) {
|
|
@@ -8315,28 +8678,28 @@ function readActiveRecovery(db, targetRevision) {
|
|
|
8315
8678
|
}
|
|
8316
8679
|
function requireRecoveryId(recoveryId) {
|
|
8317
8680
|
if (typeof recoveryId !== "string" || !isLowercaseUuid(recoveryId)) {
|
|
8318
|
-
|
|
8681
|
+
invalid2("recovery_id is invalid");
|
|
8319
8682
|
}
|
|
8320
8683
|
}
|
|
8321
8684
|
function requireNow(now) {
|
|
8322
8685
|
if (!Number.isSafeInteger(now) || now < 0 || now > Number.MAX_SAFE_INTEGER)
|
|
8323
|
-
|
|
8686
|
+
invalid2("recovery timestamp is invalid");
|
|
8324
8687
|
}
|
|
8325
8688
|
function initialRetryAt(trigger, now) {
|
|
8326
8689
|
if (trigger === "manual")
|
|
8327
8690
|
return null;
|
|
8328
8691
|
if (now > Number.MAX_SAFE_INTEGER - RECOVERY_INITIAL_DELAY_MS)
|
|
8329
|
-
|
|
8692
|
+
invalid2("automatic recovery retry timestamp would overflow");
|
|
8330
8693
|
return now + RECOVERY_INITIAL_DELAY_MS;
|
|
8331
8694
|
}
|
|
8332
8695
|
function requireAttempt(attemptCount) {
|
|
8333
8696
|
if (!Number.isSafeInteger(attemptCount) || attemptCount < 0 || attemptCount > RECOVERY_MAX_ATTEMPTS) {
|
|
8334
|
-
|
|
8697
|
+
invalid2("recovery attempt count is invalid");
|
|
8335
8698
|
}
|
|
8336
8699
|
}
|
|
8337
8700
|
function requireDetail(detail) {
|
|
8338
8701
|
if (detail !== null && (detail.length > 512 || detail.trim().length === 0))
|
|
8339
|
-
|
|
8702
|
+
invalid2("recovery reason detail is invalid");
|
|
8340
8703
|
}
|
|
8341
8704
|
function requireReasonCompatible(db, recovery, reason) {
|
|
8342
8705
|
if (reason !== "deterministic_worker_rejection" && reason !== "deterministic_protocol_failure" && reason !== "deterministic_control_failure" && reason !== "fatal_source_failure")
|
|
@@ -8349,12 +8712,12 @@ function requireReasonCompatible(db, recovery, reason) {
|
|
|
8349
8712
|
function requireRevisionDrift(db, recovery) {
|
|
8350
8713
|
const currentRevision = sqliteGet(db, "SELECT active_revision FROM configuration_state WHERE id=1")?.active_revision;
|
|
8351
8714
|
if (currentRevision === undefined || currentRevision <= recovery.target_revision)
|
|
8352
|
-
|
|
8715
|
+
invalid2("revision has not been superseded");
|
|
8353
8716
|
}
|
|
8354
8717
|
function requireTimestamp2(now, recovery) {
|
|
8355
8718
|
requireNow(now);
|
|
8356
8719
|
if (now < recovery.updated_at)
|
|
8357
|
-
|
|
8720
|
+
invalid2("recovery timestamp regressed");
|
|
8358
8721
|
}
|
|
8359
8722
|
function sameIdentity(recovery, trigger, sourceMutationId, targetRevision) {
|
|
8360
8723
|
return recovery.trigger === trigger && recovery.source_mutation_id === sourceMutationId && recovery.target_revision === targetRevision;
|
|
@@ -8377,7 +8740,7 @@ function requireNoActivePublication(db) {
|
|
|
8377
8740
|
WHERE committed_revision=(SELECT active_revision FROM configuration_state WHERE id=1)
|
|
8378
8741
|
AND state IN ('committed','publishing','draining') LIMIT 1`);
|
|
8379
8742
|
if (active !== null)
|
|
8380
|
-
|
|
8743
|
+
invalid2("an active publication already exists");
|
|
8381
8744
|
}
|
|
8382
8745
|
function insertRecovery(db, recoveryId, sourceMutationId, targetRevision, trigger, now, initial = { state: "scheduled", final_reason_code: null, final_reason_detail: null }) {
|
|
8383
8746
|
requireRecoveryId(recoveryId);
|
|
@@ -8395,14 +8758,14 @@ function insertRecovery(db, recoveryId, sourceMutationId, targetRevision, trigge
|
|
|
8395
8758
|
}
|
|
8396
8759
|
const latest = readLatestRecovery(db, targetRevision);
|
|
8397
8760
|
if (latest !== null && now < latest.updated_at)
|
|
8398
|
-
|
|
8761
|
+
invalid2("recovery timestamp regressed");
|
|
8399
8762
|
const latestCreatedAt = sqliteGet(db, "SELECT created_at FROM configuration_recoveries ORDER BY recovery_sequence DESC LIMIT 1")?.created_at;
|
|
8400
8763
|
if (latestCreatedAt !== null && latestCreatedAt !== undefined && now < latestCreatedAt) {
|
|
8401
|
-
|
|
8764
|
+
invalid2("recovery creation timestamp regressed");
|
|
8402
8765
|
}
|
|
8403
8766
|
const nextRetryAt = initial.state === "scheduled" ? initialRetryAt(trigger, now) : null;
|
|
8404
8767
|
if (initial.state === "stopped" && (initial.final_reason_code === null || initial.final_reason_code === "target_serving" || initial.final_reason_code === "already_serving" || initial.final_reason_code === "retry_exhausted" || initial.final_reason_code === "revision_superseded" || initial.final_reason_code === "safety_outcome_unknown")) {
|
|
8405
|
-
|
|
8768
|
+
invalid2("initial stopped recovery reason is invalid");
|
|
8406
8769
|
}
|
|
8407
8770
|
if (initial.final_reason_detail !== null)
|
|
8408
8771
|
requireDetail(initial.final_reason_detail);
|
|
@@ -8454,7 +8817,7 @@ function createManualRecovery(db, recoveryId, sourceMutationId, expectedRevision
|
|
|
8454
8817
|
function claimRecoveryAttempt(db, recoveryId, previousAttemptCount, now) {
|
|
8455
8818
|
const recovery = readRecovery(db, recoveryId);
|
|
8456
8819
|
if (recovery === null)
|
|
8457
|
-
|
|
8820
|
+
invalid2("recovery was not found");
|
|
8458
8821
|
requireCurrentTarget(db, recovery);
|
|
8459
8822
|
requireAttempt(previousAttemptCount);
|
|
8460
8823
|
requireNow(now);
|
|
@@ -8483,7 +8846,7 @@ function claimRecoveryAttempt(db, recoveryId, previousAttemptCount, now) {
|
|
|
8483
8846
|
function scheduleRecoveryRetry(db, recoveryId, attemptCount, nextRetryAt, now) {
|
|
8484
8847
|
const recovery = readRecovery(db, recoveryId);
|
|
8485
8848
|
if (recovery === null)
|
|
8486
|
-
|
|
8849
|
+
invalid2("recovery was not found");
|
|
8487
8850
|
requireCurrentTarget(db, recovery);
|
|
8488
8851
|
requireAttempt(attemptCount);
|
|
8489
8852
|
requireNow(nextRetryAt);
|
|
@@ -8496,7 +8859,7 @@ function scheduleRecoveryRetry(db, recoveryId, attemptCount, nextRetryAt, now) {
|
|
|
8496
8859
|
throw new ConfigRepositoryError("attempts_exhausted", "recovery attempt limit reached");
|
|
8497
8860
|
}
|
|
8498
8861
|
if (nextRetryAt <= now)
|
|
8499
|
-
|
|
8862
|
+
invalid2("next retry time must be in the future");
|
|
8500
8863
|
const result = db.run(`UPDATE configuration_recoveries SET state='scheduled',next_retry_at=?,updated_at=?
|
|
8501
8864
|
WHERE recovery_id=? AND state='running' AND attempt_count=?`, [nextRetryAt, now, recoveryId, attemptCount]);
|
|
8502
8865
|
if (result.changes !== 1)
|
|
@@ -8509,9 +8872,9 @@ function scheduleRecoveryRetry(db, recoveryId, attemptCount, nextRetryAt, now) {
|
|
|
8509
8872
|
function terminalTransition(db, recoveryId, attemptCount, state, finalReasonCode, finalReasonDetail, now) {
|
|
8510
8873
|
const recovery = readRecovery(db, recoveryId);
|
|
8511
8874
|
if (recovery === null)
|
|
8512
|
-
|
|
8875
|
+
invalid2("recovery was not found");
|
|
8513
8876
|
requireAttempt(attemptCount);
|
|
8514
|
-
validateTerminalMatrix(state, attemptCount, finalReasonCode,
|
|
8877
|
+
validateTerminalMatrix(state, attemptCount, finalReasonCode, invalid2);
|
|
8515
8878
|
requireDetail(finalReasonDetail);
|
|
8516
8879
|
if (recovery.state === state && recovery.attempt_count === attemptCount && recovery.final_reason_code === finalReasonCode && recovery.final_reason_detail === finalReasonDetail)
|
|
8517
8880
|
return recovery;
|
|
@@ -8542,7 +8905,7 @@ function stopRecovery(db, recoveryId, attemptCount, finalReasonCode, finalReason
|
|
|
8542
8905
|
function requeueRecovery(db, recoveryId, attemptCount, now) {
|
|
8543
8906
|
const recovery = readRecovery(db, recoveryId);
|
|
8544
8907
|
if (recovery === null)
|
|
8545
|
-
|
|
8908
|
+
invalid2("recovery was not found");
|
|
8546
8909
|
requireCurrentTarget(db, recovery);
|
|
8547
8910
|
requireAttempt(attemptCount);
|
|
8548
8911
|
requireNow(now);
|
|
@@ -9027,8 +9390,88 @@ var CONFIG_BASELINE = {
|
|
|
9027
9390
|
}
|
|
9028
9391
|
};
|
|
9029
9392
|
|
|
9393
|
+
// src/config-storage/migrations/publication-retirement.ts
|
|
9394
|
+
var PUBLICATION_RETIREMENT_MIGRATION = {
|
|
9395
|
+
version: 2,
|
|
9396
|
+
name: "publication_retirement_and_runtime_recovery",
|
|
9397
|
+
up(db) {
|
|
9398
|
+
db.run(`CREATE TABLE configuration_retirements (
|
|
9399
|
+
mutation_id TEXT PRIMARY KEY REFERENCES configuration_operations(mutation_id),
|
|
9400
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
9401
|
+
members_json TEXT NOT NULL CHECK(json_valid(members_json) AND json_type(members_json)='array'),
|
|
9402
|
+
state TEXT NOT NULL CHECK(state IN ('pending','stopped','released')),
|
|
9403
|
+
reason_code TEXT CHECK(reason_code IS NULL OR reason_code IN ('exit_unknown','cleanup_unknown','release_unknown')),
|
|
9404
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= 0)
|
|
9405
|
+
) STRICT`);
|
|
9406
|
+
db.run(`CREATE TABLE configuration_retirement_evidence (
|
|
9407
|
+
mutation_id TEXT NOT NULL REFERENCES configuration_retirements(mutation_id),
|
|
9408
|
+
member_id TEXT NOT NULL,
|
|
9409
|
+
exit_state TEXT NOT NULL CHECK(exit_state IN ('live','confirmed','unknown')),
|
|
9410
|
+
cleanup_state TEXT NOT NULL CHECK(cleanup_state IN ('success','failed','unknown')),
|
|
9411
|
+
exit_boot_id TEXT,
|
|
9412
|
+
exit_deadline_ns TEXT,
|
|
9413
|
+
start_boot_id TEXT,
|
|
9414
|
+
start_deadline_ns TEXT,
|
|
9415
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= 0),
|
|
9416
|
+
CHECK((exit_boot_id IS NULL AND exit_deadline_ns IS NULL) OR (exit_boot_id IS NOT NULL AND exit_deadline_ns IS NOT NULL)),
|
|
9417
|
+
CHECK((start_boot_id IS NULL AND start_deadline_ns IS NULL) OR (start_boot_id IS NOT NULL AND start_deadline_ns IS NOT NULL)),
|
|
9418
|
+
PRIMARY KEY(mutation_id,member_id)
|
|
9419
|
+
) STRICT, WITHOUT ROWID`);
|
|
9420
|
+
db.run("CREATE TRIGGER retirement_start_deadline_immutable BEFORE UPDATE OF start_boot_id,start_deadline_ns ON configuration_retirement_evidence WHEN OLD.start_deadline_ns IS NOT NULL AND (NEW.start_boot_id IS NOT OLD.start_boot_id OR NEW.start_deadline_ns IS NOT OLD.start_deadline_ns) BEGIN SELECT RAISE(ABORT,'retirement start deadline is immutable'); END");
|
|
9421
|
+
db.run(`CREATE TABLE configuration_publication_serving_sets (
|
|
9422
|
+
mutation_id TEXT NOT NULL REFERENCES configuration_operations(mutation_id),
|
|
9423
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
9424
|
+
attempt_key TEXT NOT NULL CHECK(json_valid(attempt_key) AND json_type(attempt_key)='array'),
|
|
9425
|
+
workers_json TEXT NOT NULL CHECK(json_valid(workers_json) AND json_type(workers_json)='array'),
|
|
9426
|
+
exited_json TEXT NOT NULL CHECK(json_valid(exited_json) AND json_type(exited_json)='array'),
|
|
9427
|
+
PRIMARY KEY(mutation_id,generation,attempt_key)
|
|
9428
|
+
) STRICT, WITHOUT ROWID`);
|
|
9429
|
+
db.run("CREATE TRIGGER publication_serving_authority_no_delete BEFORE DELETE ON configuration_publication_serving_sets BEGIN SELECT RAISE(ABORT,'publication serving authority history is immutable'); END");
|
|
9430
|
+
db.run("CREATE TRIGGER publication_serving_authority_no_update BEFORE UPDATE OF mutation_id,generation,attempt_key,workers_json ON configuration_publication_serving_sets BEGIN SELECT RAISE(ABORT,'publication serving attempt identity is immutable'); END");
|
|
9431
|
+
db.run("CREATE TRIGGER publication_serving_authority_no_replace BEFORE INSERT ON configuration_publication_serving_sets WHEN EXISTS(SELECT 1 FROM configuration_publication_serving_sets WHERE mutation_id=NEW.mutation_id AND generation=NEW.generation AND attempt_key=NEW.attempt_key) BEGIN SELECT RAISE(ABORT,'publication serving authority history is immutable'); END");
|
|
9432
|
+
db.run(`CREATE TABLE runtime_recovery_budget (
|
|
9433
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9434
|
+
tickets INTEGER NOT NULL CHECK(tickets BETWEEN 0 AND 2),
|
|
9435
|
+
ticket_id TEXT,
|
|
9436
|
+
ticket_boot_id TEXT,
|
|
9437
|
+
retry_at_ns TEXT,
|
|
9438
|
+
healthy_owner TEXT,
|
|
9439
|
+
healthy_boot_id TEXT,
|
|
9440
|
+
healthy_since_ns TEXT,
|
|
9441
|
+
healthy_last_ns TEXT,
|
|
9442
|
+
CHECK((tickets=0 AND ticket_id IS NULL AND ticket_boot_id IS NULL AND retry_at_ns IS NULL) OR
|
|
9443
|
+
(tickets>0 AND ticket_id IS NOT NULL AND ticket_boot_id IS NOT NULL AND retry_at_ns IS NOT NULL)),
|
|
9444
|
+
CHECK((healthy_owner IS NULL AND healthy_boot_id IS NULL AND healthy_since_ns IS NULL AND healthy_last_ns IS NULL) OR
|
|
9445
|
+
(healthy_owner IS NOT NULL AND healthy_boot_id IS NOT NULL AND healthy_since_ns IS NOT NULL AND healthy_last_ns IS NOT NULL))
|
|
9446
|
+
) STRICT`);
|
|
9447
|
+
db.run("INSERT INTO runtime_recovery_budget(id,tickets) VALUES(1,0)");
|
|
9448
|
+
db.run(`CREATE TABLE configuration_retirement_retry_requests (
|
|
9449
|
+
request_id TEXT PRIMARY KEY,
|
|
9450
|
+
mutation_id TEXT NOT NULL REFERENCES configuration_retirements(mutation_id),
|
|
9451
|
+
expected_revision INTEGER NOT NULL CHECK(expected_revision>0),
|
|
9452
|
+
created_at INTEGER NOT NULL CHECK(created_at>=0)
|
|
9453
|
+
) STRICT, WITHOUT ROWID`);
|
|
9454
|
+
db.run("CREATE TRIGGER retirement_retry_no_delete BEFORE DELETE ON configuration_retirement_retry_requests BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
9455
|
+
db.run("CREATE TRIGGER retirement_retry_no_update BEFORE UPDATE ON configuration_retirement_retry_requests BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
9456
|
+
db.run("CREATE TRIGGER retirement_retry_no_replace BEFORE INSERT ON configuration_retirement_retry_requests WHEN EXISTS(SELECT 1 FROM configuration_retirement_retry_requests WHERE request_id=NEW.request_id) BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
9457
|
+
db.run(`CREATE TABLE runtime_recovery_tickets (
|
|
9458
|
+
ticket_id TEXT PRIMARY KEY,
|
|
9459
|
+
owner TEXT NOT NULL,
|
|
9460
|
+
boot_id TEXT NOT NULL,
|
|
9461
|
+
consumed_at_ns TEXT NOT NULL,
|
|
9462
|
+
retry_at_ns TEXT NOT NULL
|
|
9463
|
+
) STRICT, WITHOUT ROWID`);
|
|
9464
|
+
db.run("CREATE TRIGGER runtime_recovery_tickets_no_delete BEFORE DELETE ON runtime_recovery_tickets BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
9465
|
+
db.run("CREATE TRIGGER runtime_recovery_tickets_no_update BEFORE UPDATE ON runtime_recovery_tickets BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
9466
|
+
db.run("CREATE TRIGGER runtime_recovery_tickets_no_replace BEFORE INSERT ON runtime_recovery_tickets WHEN EXISTS(SELECT 1 FROM runtime_recovery_tickets WHERE ticket_id=NEW.ticket_id) BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
9467
|
+
db.run("CREATE TRIGGER runtime_recovery_budget_no_delete BEFORE DELETE ON runtime_recovery_budget BEGIN SELECT RAISE(ABORT,'runtime recovery budget cannot be deleted'); END");
|
|
9468
|
+
db.run("CREATE TRIGGER runtime_recovery_budget_no_replace BEFORE INSERT ON runtime_recovery_budget WHEN EXISTS(SELECT 1 FROM runtime_recovery_budget) BEGIN SELECT RAISE(ABORT,'runtime recovery budget cannot be replaced'); END");
|
|
9469
|
+
db.run("CREATE TRIGGER runtime_recovery_budget_fixed_identity BEFORE UPDATE OF id ON runtime_recovery_budget BEGIN SELECT RAISE(ABORT,'runtime recovery budget identity is immutable'); END");
|
|
9470
|
+
}
|
|
9471
|
+
};
|
|
9472
|
+
|
|
9030
9473
|
// src/config-storage/migrations/plan.ts
|
|
9031
|
-
var CONFIG_MIGRATIONS = [CONFIG_BASELINE];
|
|
9474
|
+
var CONFIG_MIGRATIONS = [CONFIG_BASELINE, PUBLICATION_RETIREMENT_MIGRATION];
|
|
9032
9475
|
var CONFIG_SCHEMA_VERSION = CONFIG_MIGRATIONS.at(-1).version;
|
|
9033
9476
|
|
|
9034
9477
|
// src/config-storage/schema-fingerprint.ts
|
|
@@ -9444,7 +9887,7 @@ function verifyRepositoryIntegrity(db) {
|
|
|
9444
9887
|
|
|
9445
9888
|
// src/config-storage/serving-snapshot.ts
|
|
9446
9889
|
var MAX_AGGREGATE_BYTES = 1048576;
|
|
9447
|
-
function
|
|
9890
|
+
function invalid3(message) {
|
|
9448
9891
|
throw new ConfigRepositoryError("invalid_configuration", message);
|
|
9449
9892
|
}
|
|
9450
9893
|
function corrupt3(message) {
|
|
@@ -9453,26 +9896,26 @@ function corrupt3(message) {
|
|
|
9453
9896
|
function canonicalAggregate(aggregate) {
|
|
9454
9897
|
const result = parseNormalizeCompileAggregate(aggregate, undefined);
|
|
9455
9898
|
if (!result.ok)
|
|
9456
|
-
|
|
9899
|
+
invalid3("configuration aggregate is invalid");
|
|
9457
9900
|
const aggregateJson = canonicalJson(result.value);
|
|
9458
9901
|
const bytes = new TextEncoder().encode(aggregateJson).byteLength;
|
|
9459
9902
|
if (bytes < 2 || bytes > MAX_AGGREGATE_BYTES) {
|
|
9460
|
-
|
|
9903
|
+
invalid3("configuration aggregate is too large");
|
|
9461
9904
|
}
|
|
9462
9905
|
return { aggregateJson, contentHash: hashConfigurationContent(result.value) };
|
|
9463
9906
|
}
|
|
9464
9907
|
function appendServingSnapshot(db, snapshot, pluginCatalogHash) {
|
|
9465
9908
|
if (!Number.isSafeInteger(snapshot?.revision) || snapshot.revision <= 0 || typeof snapshot.content_hash !== "string" || !validateDigest(snapshot.content_hash) || !validateDigest(pluginCatalogHash)) {
|
|
9466
|
-
|
|
9909
|
+
invalid3("serving snapshot identity is invalid");
|
|
9467
9910
|
}
|
|
9468
9911
|
const normalized = canonicalAggregate(snapshot.aggregate);
|
|
9469
9912
|
if (normalized.contentHash !== snapshot.content_hash) {
|
|
9470
|
-
|
|
9913
|
+
invalid3("configuration aggregate content hash does not match");
|
|
9471
9914
|
}
|
|
9472
9915
|
const transaction = db.transaction(() => {
|
|
9473
9916
|
const revision = sqliteGet(db, "SELECT revision,content_hash FROM configuration_revisions WHERE revision=?", snapshot.revision);
|
|
9474
9917
|
if (revision === null || revision.content_hash !== snapshot.content_hash) {
|
|
9475
|
-
|
|
9918
|
+
invalid3("serving snapshot revision does not match configuration history");
|
|
9476
9919
|
}
|
|
9477
9920
|
const existing = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
9478
9921
|
FROM configuration_serving_snapshots
|
|
@@ -9490,7 +9933,7 @@ function appendServingSnapshot(db, snapshot, pluginCatalogHash) {
|
|
|
9490
9933
|
}
|
|
9491
9934
|
function getServingSnapshot(db, key) {
|
|
9492
9935
|
if (!Number.isSafeInteger(key?.revision) || key.revision <= 0 || typeof key.content_hash !== "string" || !validateDigest(key.content_hash) || typeof key.plugin_catalog_hash !== "string" || !validateDigest(key.plugin_catalog_hash)) {
|
|
9493
|
-
|
|
9936
|
+
invalid3("serving snapshot lookup identity is invalid");
|
|
9494
9937
|
}
|
|
9495
9938
|
return withConsistentRead(db, () => {
|
|
9496
9939
|
const row = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
@@ -9741,7 +10184,7 @@ class ConfigRepository {
|
|
|
9741
10184
|
static open(dbPath, options = {}) {
|
|
9742
10185
|
let db;
|
|
9743
10186
|
try {
|
|
9744
|
-
mkdirSync(
|
|
10187
|
+
mkdirSync(dirname2(dbPath), { recursive: true });
|
|
9745
10188
|
db = new Database3(dbPath, { create: true, readwrite: true, strict: true });
|
|
9746
10189
|
configureConnection(db);
|
|
9747
10190
|
migrateConfigurationDatabase(db);
|
|
@@ -9749,6 +10192,7 @@ class ConfigRepository {
|
|
|
9749
10192
|
const repository = new ConfigRepository(db, options, freezeSnapshot(readRepositorySnapshot(db)));
|
|
9750
10193
|
repository.getSnapshot();
|
|
9751
10194
|
repository.getSupervisionState();
|
|
10195
|
+
repository.getRuntimeRecoveryBudget();
|
|
9752
10196
|
return repository;
|
|
9753
10197
|
} catch (error) {
|
|
9754
10198
|
db?.close(true);
|
|
@@ -9896,12 +10340,18 @@ class ConfigRepository {
|
|
|
9896
10340
|
}
|
|
9897
10341
|
beginWorkerAttempt(mutationId, workerSlot, previousAttemptNo, reason, updatedAt) {
|
|
9898
10342
|
return runRepositoryAction(() => this.publicationTransaction(mutationId, () => {
|
|
10343
|
+
const current = readOperationWorkers(this.db, mutationId).find((target) => target.worker_slot === workerSlot);
|
|
10344
|
+
if (current?.attempt_no === previousAttemptNo)
|
|
10345
|
+
assertPublicationServingAttemptsExited(this.db, mutationId);
|
|
9899
10346
|
const worker = beginWorkerAttempt(this.db, mutationId, workerSlot, previousAttemptNo, reason, updatedAt);
|
|
9900
10347
|
return worker;
|
|
9901
10348
|
}));
|
|
9902
10349
|
}
|
|
9903
10350
|
beginDrainingRecovery(mutationId, previousGeneration, updatedAt) {
|
|
9904
10351
|
return runRepositoryAction(() => this.publicationTransaction(mutationId, () => {
|
|
10352
|
+
if (getOperation(this.db, mutationId)?.drain_recovery_generation === previousGeneration) {
|
|
10353
|
+
assertPublicationServingAttemptsExited(this.db, mutationId);
|
|
10354
|
+
}
|
|
9905
10355
|
const operation = beginDrainingRecovery(this.db, mutationId, previousGeneration, updatedAt);
|
|
9906
10356
|
return operation;
|
|
9907
10357
|
}));
|
|
@@ -9945,9 +10395,68 @@ class ConfigRepository {
|
|
|
9945
10395
|
return operation;
|
|
9946
10396
|
}));
|
|
9947
10397
|
}
|
|
10398
|
+
getPublicationServingSet(...args) {
|
|
10399
|
+
return runRepositoryAction(() => withConsistentRead(this.db, () => readPublicationServingSet(this.db, ...args)));
|
|
10400
|
+
}
|
|
10401
|
+
freezePublicationServingSet(...args) {
|
|
10402
|
+
return runRepositoryAction(() => this.mutationTransaction(() => freezePublicationServingSet(this.db, ...args)));
|
|
10403
|
+
}
|
|
10404
|
+
confirmPublicationServingMemberExit(...args) {
|
|
10405
|
+
return runRepositoryAction(() => this.mutationTransaction(() => confirmPublicationServingMemberExit(this.db, ...args)));
|
|
10406
|
+
}
|
|
10407
|
+
getRetirementDescriptorCleanupState(workerInstanceId) {
|
|
10408
|
+
return runRepositoryAction(() => withConsistentRead(this.db, () => readRetirementDescriptorCleanupState(this.db, workerInstanceId)));
|
|
10409
|
+
}
|
|
10410
|
+
getRetiredWorkerInstanceIds() {
|
|
10411
|
+
return runRepositoryAction(() => withConsistentRead(this.db, () => readRetiredWorkerInstanceIds(this.db)));
|
|
10412
|
+
}
|
|
10413
|
+
getPublicationRetirement(...args) {
|
|
10414
|
+
return runRepositoryAction(() => withConsistentRead(this.db, () => readPublicationRetirement(this.db, ...args)));
|
|
10415
|
+
}
|
|
10416
|
+
freezePublicationRetirement(...args) {
|
|
10417
|
+
return runRepositoryAction(() => this.publicationTransaction(args[0], () => freezePublicationRetirement(this.db, ...args)));
|
|
10418
|
+
}
|
|
10419
|
+
recordPublicationRetirementEvidence(...args) {
|
|
10420
|
+
return runRepositoryAction(() => this.mutationTransaction(() => recordPublicationRetirementEvidence(this.db, ...args)));
|
|
10421
|
+
}
|
|
10422
|
+
stopPublicationRetirement(...args) {
|
|
10423
|
+
return runRepositoryAction(() => this.mutationTransaction(() => stopPublicationRetirement(this.db, ...args)));
|
|
10424
|
+
}
|
|
10425
|
+
retryPublicationRetirement(...args) {
|
|
10426
|
+
return runRepositoryAction(() => this.mutationTransaction(() => retryPublicationRetirement(this.db, ...args)));
|
|
10427
|
+
}
|
|
10428
|
+
freezePublicationRetirementDrainStart(...args) {
|
|
10429
|
+
return runRepositoryAction(() => this.mutationTransaction(() => freezePublicationRetirementDrainStart(this.db, ...args)));
|
|
10430
|
+
}
|
|
10431
|
+
getPublicationRetirementRetry(...args) {
|
|
10432
|
+
return runRepositoryAction(() => withConsistentRead(this.db, () => readPublicationRetirementRetry(this.db, ...args)));
|
|
10433
|
+
}
|
|
10434
|
+
requestPublicationRetirementRetry(...args) {
|
|
10435
|
+
return runRepositoryAction(() => this.mutationTransaction(() => {
|
|
10436
|
+
if (this.snapshot.revision !== args[2])
|
|
10437
|
+
throw new ConfigRepositoryError("cas_conflict", "retirement retry revision changed");
|
|
10438
|
+
return requestPublicationRetirementRetry(this.db, ...args);
|
|
10439
|
+
}));
|
|
10440
|
+
}
|
|
10441
|
+
markPublicationRetirementReleased(...args) {
|
|
10442
|
+
return runRepositoryAction(() => this.mutationTransaction(() => markPublicationRetirementReleased(this.db, ...args)));
|
|
10443
|
+
}
|
|
10444
|
+
getRuntimeRecoveryBudget(...args) {
|
|
10445
|
+
return runRepositoryAction(() => withConsistentRead(this.db, () => readRuntimeRecoveryBudget(this.db, ...args)));
|
|
10446
|
+
}
|
|
10447
|
+
consumeRuntimeRecoveryRestartTicket(...args) {
|
|
10448
|
+
return runRepositoryAction(() => this.mutationTransaction(() => consumeRuntimeRecoveryRestartTicket(this.db, ...args)));
|
|
10449
|
+
}
|
|
10450
|
+
getRuntimeRecoveryStartupGate(...args) {
|
|
10451
|
+
return runRepositoryAction(() => withConsistentRead(this.db, () => readRuntimeRecoveryStartupGate(this.db, ...args)));
|
|
10452
|
+
}
|
|
10453
|
+
observeRuntimeRecoveryHealth(...args) {
|
|
10454
|
+
return runRepositoryAction(() => this.mutationTransaction(() => observeRuntimeRecoveryHealth(this.db, ...args)));
|
|
10455
|
+
}
|
|
9948
10456
|
verify() {
|
|
9949
10457
|
verifyRepositoryIntegrity(this.db);
|
|
9950
10458
|
readRepositorySnapshot(this.db);
|
|
10459
|
+
this.getRuntimeRecoveryBudget();
|
|
9951
10460
|
}
|
|
9952
10461
|
claimController(controllerId, updatedAt) {
|
|
9953
10462
|
return runRepositoryAction(() => this.mutationTransaction(() => this.supervision.claim(controllerId, updatedAt)));
|
|
@@ -9980,6 +10489,9 @@ class ConfigRepository {
|
|
|
9980
10489
|
|
|
9981
10490
|
// src/config-storage/storage-protocol.ts
|
|
9982
10491
|
var STORAGE_METHODS = [
|
|
10492
|
+
"getPublicationRetirementRetry",
|
|
10493
|
+
"requestPublicationRetirementRetry",
|
|
10494
|
+
"freezePublicationRetirementDrainStart",
|
|
9983
10495
|
"commitPrepared",
|
|
9984
10496
|
"appendServingSnapshot",
|
|
9985
10497
|
"getServingSnapshot",
|
|
@@ -10003,9 +10515,26 @@ var STORAGE_METHODS = [
|
|
|
10003
10515
|
"recordWorkerResult",
|
|
10004
10516
|
"finalizePublication",
|
|
10005
10517
|
"markDraining",
|
|
10006
|
-
"verify"
|
|
10518
|
+
"verify",
|
|
10519
|
+
"getPublicationServingSet",
|
|
10520
|
+
"freezePublicationServingSet",
|
|
10521
|
+
"confirmPublicationServingMemberExit",
|
|
10522
|
+
"getRetirementDescriptorCleanupState",
|
|
10523
|
+
"getRetiredWorkerInstanceIds",
|
|
10524
|
+
"getPublicationRetirement",
|
|
10525
|
+
"freezePublicationRetirement",
|
|
10526
|
+
"recordPublicationRetirementEvidence",
|
|
10527
|
+
"stopPublicationRetirement",
|
|
10528
|
+
"retryPublicationRetirement",
|
|
10529
|
+
"markPublicationRetirementReleased",
|
|
10530
|
+
"getRuntimeRecoveryBudget",
|
|
10531
|
+
"consumeRuntimeRecoveryRestartTicket",
|
|
10532
|
+
"getRuntimeRecoveryStartupGate",
|
|
10533
|
+
"observeRuntimeRecoveryHealth"
|
|
10007
10534
|
];
|
|
10008
10535
|
var READ_METHODS = new Set([
|
|
10536
|
+
"getPublicationRetirementRetry",
|
|
10537
|
+
"getPublicationServingSet",
|
|
10009
10538
|
"getServingSnapshot",
|
|
10010
10539
|
"getActivePublication",
|
|
10011
10540
|
"getOperation",
|
|
@@ -10014,7 +10543,12 @@ var READ_METHODS = new Set([
|
|
|
10014
10543
|
"getRecovery",
|
|
10015
10544
|
"getCurrentRecovery",
|
|
10016
10545
|
"getLatestRecovery",
|
|
10017
|
-
"verify"
|
|
10546
|
+
"verify",
|
|
10547
|
+
"getRetirementDescriptorCleanupState",
|
|
10548
|
+
"getRetiredWorkerInstanceIds",
|
|
10549
|
+
"getPublicationRetirement",
|
|
10550
|
+
"getRuntimeRecoveryBudget",
|
|
10551
|
+
"getRuntimeRecoveryStartupGate"
|
|
10018
10552
|
]);
|
|
10019
10553
|
function safeStorageError(error) {
|
|
10020
10554
|
const code = error instanceof ConfigRepositoryError ? error.code : "repository_failure";
|