@jeffusion/bungee-core 5.23.2 → 5.24.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/dist/config-storage-worker.js +924 -392
- package/dist/main.js +2480 -588
- package/dist/plugin-state-worker.js +178 -16
- package/dist/plugins/chatgpt-oauth/index.js +7 -1
- package/dist/plugins/chatgpt-oauth/index.js.map +3 -3
- package/dist/plugins/chatgpt-oauth/manifest.json +17 -0
- 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
|
|
@@ -7543,6 +7571,417 @@ function validateCommitCommand(command, compileOptions) {
|
|
|
7543
7571
|
return result.value;
|
|
7544
7572
|
}
|
|
7545
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
|
+
|
|
7546
7985
|
// src/config-storage/operation-records.ts
|
|
7547
7986
|
var OPERATION_SELECT = `SELECT mutation_id,request_hash,expected_revision,committed_revision,kind,state,
|
|
7548
7987
|
target_worker_count,result_status,error_code,error_detail,drain_recovery_generation,
|
|
@@ -7892,84 +8331,6 @@ function readActiveRevision(db) {
|
|
|
7892
8331
|
return revision;
|
|
7893
8332
|
}
|
|
7894
8333
|
|
|
7895
|
-
// ../../node_modules/canonicalize/lib/canonicalize.js
|
|
7896
|
-
function canonicalize(object, seen = new Set) {
|
|
7897
|
-
if (typeof object === "number" && isNaN(object)) {
|
|
7898
|
-
throw new Error("NaN is not allowed");
|
|
7899
|
-
}
|
|
7900
|
-
if (typeof object === "number" && !isFinite(object)) {
|
|
7901
|
-
throw new Error("Infinity is not allowed");
|
|
7902
|
-
}
|
|
7903
|
-
if (object === null || typeof object !== "object") {
|
|
7904
|
-
return JSON.stringify(object);
|
|
7905
|
-
}
|
|
7906
|
-
if (typeof object.toJSON === "function") {
|
|
7907
|
-
if (seen.has(object)) {
|
|
7908
|
-
throw new Error("Circular reference detected");
|
|
7909
|
-
}
|
|
7910
|
-
seen.add(object);
|
|
7911
|
-
const result = canonicalize(object.toJSON(), seen);
|
|
7912
|
-
seen.delete(object);
|
|
7913
|
-
return result;
|
|
7914
|
-
}
|
|
7915
|
-
if (seen.has(object)) {
|
|
7916
|
-
throw new Error("Circular reference detected");
|
|
7917
|
-
}
|
|
7918
|
-
seen.add(object);
|
|
7919
|
-
let result;
|
|
7920
|
-
if (Array.isArray(object)) {
|
|
7921
|
-
const values = object.map((cv) => {
|
|
7922
|
-
const value = cv === undefined || typeof cv === "symbol" ? null : cv;
|
|
7923
|
-
return canonicalize(value, seen);
|
|
7924
|
-
});
|
|
7925
|
-
result = `[${values.join(",")}]`;
|
|
7926
|
-
} else {
|
|
7927
|
-
const parts = [];
|
|
7928
|
-
for (const key of Object.keys(object).sort()) {
|
|
7929
|
-
if (object[key] === undefined || typeof object[key] === "symbol") {
|
|
7930
|
-
continue;
|
|
7931
|
-
}
|
|
7932
|
-
parts.push(`${canonicalize(key)}:${canonicalize(object[key], seen)}`);
|
|
7933
|
-
}
|
|
7934
|
-
result = `{${parts.join(",")}}`;
|
|
7935
|
-
}
|
|
7936
|
-
seen.delete(object);
|
|
7937
|
-
return result;
|
|
7938
|
-
}
|
|
7939
|
-
|
|
7940
|
-
// src/config-storage/content-hash.ts
|
|
7941
|
-
class ConfigurationHashError extends Error {
|
|
7942
|
-
cause;
|
|
7943
|
-
name = "ConfigurationHashError";
|
|
7944
|
-
constructor(cause) {
|
|
7945
|
-
super("RFC 8785 canonicalization failed", { cause });
|
|
7946
|
-
this.cause = cause;
|
|
7947
|
-
}
|
|
7948
|
-
}
|
|
7949
|
-
function canonicalJson(value) {
|
|
7950
|
-
try {
|
|
7951
|
-
const result = canonicalize(snapshotJsonGraph(value));
|
|
7952
|
-
if (result === undefined)
|
|
7953
|
-
throw new TypeError("canonicalize returned undefined");
|
|
7954
|
-
return result;
|
|
7955
|
-
} catch (error) {
|
|
7956
|
-
throw new ConfigurationHashError(error);
|
|
7957
|
-
}
|
|
7958
|
-
}
|
|
7959
|
-
function hashConfigurationContent(value) {
|
|
7960
|
-
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
7961
|
-
const hex = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
7962
|
-
return `sha256:${hex}`;
|
|
7963
|
-
}
|
|
7964
|
-
function hashConfigurationRequest(identity) {
|
|
7965
|
-
return hashConfigurationContent({
|
|
7966
|
-
kind: identity.kind,
|
|
7967
|
-
expected_revision: identity.expected_revision,
|
|
7968
|
-
aggregate: identity.aggregate,
|
|
7969
|
-
target_worker_slots: [...identity.target_worker_slots].sort((left, right) => left - right)
|
|
7970
|
-
});
|
|
7971
|
-
}
|
|
7972
|
-
|
|
7973
8334
|
// src/config-storage/operation-relations.ts
|
|
7974
8335
|
function validateActiveOperationIdentity(db, operation, workers, snapshot) {
|
|
7975
8336
|
validateOperationRelations(operation, workers);
|
|
@@ -8197,7 +8558,7 @@ var REASON_CODES = [
|
|
|
8197
8558
|
var RECOVERY_SELECT = `SELECT recovery_sequence,recovery_id,source_mutation_id,target_revision,trigger,state,
|
|
8198
8559
|
attempt_count,max_attempts,next_retry_at,final_reason_code,final_reason_detail,created_at,updated_at
|
|
8199
8560
|
FROM configuration_recoveries`;
|
|
8200
|
-
function
|
|
8561
|
+
function invalid2(message) {
|
|
8201
8562
|
throw new ConfigRepositoryError("invalid_operation", message);
|
|
8202
8563
|
}
|
|
8203
8564
|
function corrupt2(message) {
|
|
@@ -8317,28 +8678,28 @@ function readActiveRecovery(db, targetRevision) {
|
|
|
8317
8678
|
}
|
|
8318
8679
|
function requireRecoveryId(recoveryId) {
|
|
8319
8680
|
if (typeof recoveryId !== "string" || !isLowercaseUuid(recoveryId)) {
|
|
8320
|
-
|
|
8681
|
+
invalid2("recovery_id is invalid");
|
|
8321
8682
|
}
|
|
8322
8683
|
}
|
|
8323
8684
|
function requireNow(now) {
|
|
8324
8685
|
if (!Number.isSafeInteger(now) || now < 0 || now > Number.MAX_SAFE_INTEGER)
|
|
8325
|
-
|
|
8686
|
+
invalid2("recovery timestamp is invalid");
|
|
8326
8687
|
}
|
|
8327
8688
|
function initialRetryAt(trigger, now) {
|
|
8328
8689
|
if (trigger === "manual")
|
|
8329
8690
|
return null;
|
|
8330
8691
|
if (now > Number.MAX_SAFE_INTEGER - RECOVERY_INITIAL_DELAY_MS)
|
|
8331
|
-
|
|
8692
|
+
invalid2("automatic recovery retry timestamp would overflow");
|
|
8332
8693
|
return now + RECOVERY_INITIAL_DELAY_MS;
|
|
8333
8694
|
}
|
|
8334
8695
|
function requireAttempt(attemptCount) {
|
|
8335
8696
|
if (!Number.isSafeInteger(attemptCount) || attemptCount < 0 || attemptCount > RECOVERY_MAX_ATTEMPTS) {
|
|
8336
|
-
|
|
8697
|
+
invalid2("recovery attempt count is invalid");
|
|
8337
8698
|
}
|
|
8338
8699
|
}
|
|
8339
8700
|
function requireDetail(detail) {
|
|
8340
8701
|
if (detail !== null && (detail.length > 512 || detail.trim().length === 0))
|
|
8341
|
-
|
|
8702
|
+
invalid2("recovery reason detail is invalid");
|
|
8342
8703
|
}
|
|
8343
8704
|
function requireReasonCompatible(db, recovery, reason) {
|
|
8344
8705
|
if (reason !== "deterministic_worker_rejection" && reason !== "deterministic_protocol_failure" && reason !== "deterministic_control_failure" && reason !== "fatal_source_failure")
|
|
@@ -8351,12 +8712,12 @@ function requireReasonCompatible(db, recovery, reason) {
|
|
|
8351
8712
|
function requireRevisionDrift(db, recovery) {
|
|
8352
8713
|
const currentRevision = sqliteGet(db, "SELECT active_revision FROM configuration_state WHERE id=1")?.active_revision;
|
|
8353
8714
|
if (currentRevision === undefined || currentRevision <= recovery.target_revision)
|
|
8354
|
-
|
|
8715
|
+
invalid2("revision has not been superseded");
|
|
8355
8716
|
}
|
|
8356
8717
|
function requireTimestamp2(now, recovery) {
|
|
8357
8718
|
requireNow(now);
|
|
8358
8719
|
if (now < recovery.updated_at)
|
|
8359
|
-
|
|
8720
|
+
invalid2("recovery timestamp regressed");
|
|
8360
8721
|
}
|
|
8361
8722
|
function sameIdentity(recovery, trigger, sourceMutationId, targetRevision) {
|
|
8362
8723
|
return recovery.trigger === trigger && recovery.source_mutation_id === sourceMutationId && recovery.target_revision === targetRevision;
|
|
@@ -8379,7 +8740,7 @@ function requireNoActivePublication(db) {
|
|
|
8379
8740
|
WHERE committed_revision=(SELECT active_revision FROM configuration_state WHERE id=1)
|
|
8380
8741
|
AND state IN ('committed','publishing','draining') LIMIT 1`);
|
|
8381
8742
|
if (active !== null)
|
|
8382
|
-
|
|
8743
|
+
invalid2("an active publication already exists");
|
|
8383
8744
|
}
|
|
8384
8745
|
function insertRecovery(db, recoveryId, sourceMutationId, targetRevision, trigger, now, initial = { state: "scheduled", final_reason_code: null, final_reason_detail: null }) {
|
|
8385
8746
|
requireRecoveryId(recoveryId);
|
|
@@ -8397,14 +8758,14 @@ function insertRecovery(db, recoveryId, sourceMutationId, targetRevision, trigge
|
|
|
8397
8758
|
}
|
|
8398
8759
|
const latest = readLatestRecovery(db, targetRevision);
|
|
8399
8760
|
if (latest !== null && now < latest.updated_at)
|
|
8400
|
-
|
|
8761
|
+
invalid2("recovery timestamp regressed");
|
|
8401
8762
|
const latestCreatedAt = sqliteGet(db, "SELECT created_at FROM configuration_recoveries ORDER BY recovery_sequence DESC LIMIT 1")?.created_at;
|
|
8402
8763
|
if (latestCreatedAt !== null && latestCreatedAt !== undefined && now < latestCreatedAt) {
|
|
8403
|
-
|
|
8764
|
+
invalid2("recovery creation timestamp regressed");
|
|
8404
8765
|
}
|
|
8405
8766
|
const nextRetryAt = initial.state === "scheduled" ? initialRetryAt(trigger, now) : null;
|
|
8406
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")) {
|
|
8407
|
-
|
|
8768
|
+
invalid2("initial stopped recovery reason is invalid");
|
|
8408
8769
|
}
|
|
8409
8770
|
if (initial.final_reason_detail !== null)
|
|
8410
8771
|
requireDetail(initial.final_reason_detail);
|
|
@@ -8456,7 +8817,7 @@ function createManualRecovery(db, recoveryId, sourceMutationId, expectedRevision
|
|
|
8456
8817
|
function claimRecoveryAttempt(db, recoveryId, previousAttemptCount, now) {
|
|
8457
8818
|
const recovery = readRecovery(db, recoveryId);
|
|
8458
8819
|
if (recovery === null)
|
|
8459
|
-
|
|
8820
|
+
invalid2("recovery was not found");
|
|
8460
8821
|
requireCurrentTarget(db, recovery);
|
|
8461
8822
|
requireAttempt(previousAttemptCount);
|
|
8462
8823
|
requireNow(now);
|
|
@@ -8485,7 +8846,7 @@ function claimRecoveryAttempt(db, recoveryId, previousAttemptCount, now) {
|
|
|
8485
8846
|
function scheduleRecoveryRetry(db, recoveryId, attemptCount, nextRetryAt, now) {
|
|
8486
8847
|
const recovery = readRecovery(db, recoveryId);
|
|
8487
8848
|
if (recovery === null)
|
|
8488
|
-
|
|
8849
|
+
invalid2("recovery was not found");
|
|
8489
8850
|
requireCurrentTarget(db, recovery);
|
|
8490
8851
|
requireAttempt(attemptCount);
|
|
8491
8852
|
requireNow(nextRetryAt);
|
|
@@ -8498,7 +8859,7 @@ function scheduleRecoveryRetry(db, recoveryId, attemptCount, nextRetryAt, now) {
|
|
|
8498
8859
|
throw new ConfigRepositoryError("attempts_exhausted", "recovery attempt limit reached");
|
|
8499
8860
|
}
|
|
8500
8861
|
if (nextRetryAt <= now)
|
|
8501
|
-
|
|
8862
|
+
invalid2("next retry time must be in the future");
|
|
8502
8863
|
const result = db.run(`UPDATE configuration_recoveries SET state='scheduled',next_retry_at=?,updated_at=?
|
|
8503
8864
|
WHERE recovery_id=? AND state='running' AND attempt_count=?`, [nextRetryAt, now, recoveryId, attemptCount]);
|
|
8504
8865
|
if (result.changes !== 1)
|
|
@@ -8511,9 +8872,9 @@ function scheduleRecoveryRetry(db, recoveryId, attemptCount, nextRetryAt, now) {
|
|
|
8511
8872
|
function terminalTransition(db, recoveryId, attemptCount, state, finalReasonCode, finalReasonDetail, now) {
|
|
8512
8873
|
const recovery = readRecovery(db, recoveryId);
|
|
8513
8874
|
if (recovery === null)
|
|
8514
|
-
|
|
8875
|
+
invalid2("recovery was not found");
|
|
8515
8876
|
requireAttempt(attemptCount);
|
|
8516
|
-
validateTerminalMatrix(state, attemptCount, finalReasonCode,
|
|
8877
|
+
validateTerminalMatrix(state, attemptCount, finalReasonCode, invalid2);
|
|
8517
8878
|
requireDetail(finalReasonDetail);
|
|
8518
8879
|
if (recovery.state === state && recovery.attempt_count === attemptCount && recovery.final_reason_code === finalReasonCode && recovery.final_reason_detail === finalReasonDetail)
|
|
8519
8880
|
return recovery;
|
|
@@ -8544,7 +8905,7 @@ function stopRecovery(db, recoveryId, attemptCount, finalReasonCode, finalReason
|
|
|
8544
8905
|
function requeueRecovery(db, recoveryId, attemptCount, now) {
|
|
8545
8906
|
const recovery = readRecovery(db, recoveryId);
|
|
8546
8907
|
if (recovery === null)
|
|
8547
|
-
|
|
8908
|
+
invalid2("recovery was not found");
|
|
8548
8909
|
requireCurrentTarget(db, recovery);
|
|
8549
8910
|
requireAttempt(attemptCount);
|
|
8550
8911
|
requireNow(now);
|
|
@@ -9029,8 +9390,88 @@ var CONFIG_BASELINE = {
|
|
|
9029
9390
|
}
|
|
9030
9391
|
};
|
|
9031
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
|
+
|
|
9032
9473
|
// src/config-storage/migrations/plan.ts
|
|
9033
|
-
var CONFIG_MIGRATIONS = [CONFIG_BASELINE];
|
|
9474
|
+
var CONFIG_MIGRATIONS = [CONFIG_BASELINE, PUBLICATION_RETIREMENT_MIGRATION];
|
|
9034
9475
|
var CONFIG_SCHEMA_VERSION = CONFIG_MIGRATIONS.at(-1).version;
|
|
9035
9476
|
|
|
9036
9477
|
// src/config-storage/schema-fingerprint.ts
|
|
@@ -9446,7 +9887,7 @@ function verifyRepositoryIntegrity(db) {
|
|
|
9446
9887
|
|
|
9447
9888
|
// src/config-storage/serving-snapshot.ts
|
|
9448
9889
|
var MAX_AGGREGATE_BYTES = 1048576;
|
|
9449
|
-
function
|
|
9890
|
+
function invalid3(message) {
|
|
9450
9891
|
throw new ConfigRepositoryError("invalid_configuration", message);
|
|
9451
9892
|
}
|
|
9452
9893
|
function corrupt3(message) {
|
|
@@ -9455,26 +9896,26 @@ function corrupt3(message) {
|
|
|
9455
9896
|
function canonicalAggregate(aggregate) {
|
|
9456
9897
|
const result = parseNormalizeCompileAggregate(aggregate, undefined);
|
|
9457
9898
|
if (!result.ok)
|
|
9458
|
-
|
|
9899
|
+
invalid3("configuration aggregate is invalid");
|
|
9459
9900
|
const aggregateJson = canonicalJson(result.value);
|
|
9460
9901
|
const bytes = new TextEncoder().encode(aggregateJson).byteLength;
|
|
9461
9902
|
if (bytes < 2 || bytes > MAX_AGGREGATE_BYTES) {
|
|
9462
|
-
|
|
9903
|
+
invalid3("configuration aggregate is too large");
|
|
9463
9904
|
}
|
|
9464
9905
|
return { aggregateJson, contentHash: hashConfigurationContent(result.value) };
|
|
9465
9906
|
}
|
|
9466
9907
|
function appendServingSnapshot(db, snapshot, pluginCatalogHash) {
|
|
9467
9908
|
if (!Number.isSafeInteger(snapshot?.revision) || snapshot.revision <= 0 || typeof snapshot.content_hash !== "string" || !validateDigest(snapshot.content_hash) || !validateDigest(pluginCatalogHash)) {
|
|
9468
|
-
|
|
9909
|
+
invalid3("serving snapshot identity is invalid");
|
|
9469
9910
|
}
|
|
9470
9911
|
const normalized = canonicalAggregate(snapshot.aggregate);
|
|
9471
9912
|
if (normalized.contentHash !== snapshot.content_hash) {
|
|
9472
|
-
|
|
9913
|
+
invalid3("configuration aggregate content hash does not match");
|
|
9473
9914
|
}
|
|
9474
9915
|
const transaction = db.transaction(() => {
|
|
9475
9916
|
const revision = sqliteGet(db, "SELECT revision,content_hash FROM configuration_revisions WHERE revision=?", snapshot.revision);
|
|
9476
9917
|
if (revision === null || revision.content_hash !== snapshot.content_hash) {
|
|
9477
|
-
|
|
9918
|
+
invalid3("serving snapshot revision does not match configuration history");
|
|
9478
9919
|
}
|
|
9479
9920
|
const existing = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
9480
9921
|
FROM configuration_serving_snapshots
|
|
@@ -9492,7 +9933,7 @@ function appendServingSnapshot(db, snapshot, pluginCatalogHash) {
|
|
|
9492
9933
|
}
|
|
9493
9934
|
function getServingSnapshot(db, key) {
|
|
9494
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)) {
|
|
9495
|
-
|
|
9936
|
+
invalid3("serving snapshot lookup identity is invalid");
|
|
9496
9937
|
}
|
|
9497
9938
|
return withConsistentRead(db, () => {
|
|
9498
9939
|
const row = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
@@ -9743,7 +10184,7 @@ class ConfigRepository {
|
|
|
9743
10184
|
static open(dbPath, options = {}) {
|
|
9744
10185
|
let db;
|
|
9745
10186
|
try {
|
|
9746
|
-
mkdirSync(
|
|
10187
|
+
mkdirSync(dirname2(dbPath), { recursive: true });
|
|
9747
10188
|
db = new Database3(dbPath, { create: true, readwrite: true, strict: true });
|
|
9748
10189
|
configureConnection(db);
|
|
9749
10190
|
migrateConfigurationDatabase(db);
|
|
@@ -9751,6 +10192,7 @@ class ConfigRepository {
|
|
|
9751
10192
|
const repository = new ConfigRepository(db, options, freezeSnapshot(readRepositorySnapshot(db)));
|
|
9752
10193
|
repository.getSnapshot();
|
|
9753
10194
|
repository.getSupervisionState();
|
|
10195
|
+
repository.getRuntimeRecoveryBudget();
|
|
9754
10196
|
return repository;
|
|
9755
10197
|
} catch (error) {
|
|
9756
10198
|
db?.close(true);
|
|
@@ -9898,12 +10340,18 @@ class ConfigRepository {
|
|
|
9898
10340
|
}
|
|
9899
10341
|
beginWorkerAttempt(mutationId, workerSlot, previousAttemptNo, reason, updatedAt) {
|
|
9900
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);
|
|
9901
10346
|
const worker = beginWorkerAttempt(this.db, mutationId, workerSlot, previousAttemptNo, reason, updatedAt);
|
|
9902
10347
|
return worker;
|
|
9903
10348
|
}));
|
|
9904
10349
|
}
|
|
9905
10350
|
beginDrainingRecovery(mutationId, previousGeneration, updatedAt) {
|
|
9906
10351
|
return runRepositoryAction(() => this.publicationTransaction(mutationId, () => {
|
|
10352
|
+
if (getOperation(this.db, mutationId)?.drain_recovery_generation === previousGeneration) {
|
|
10353
|
+
assertPublicationServingAttemptsExited(this.db, mutationId);
|
|
10354
|
+
}
|
|
9907
10355
|
const operation = beginDrainingRecovery(this.db, mutationId, previousGeneration, updatedAt);
|
|
9908
10356
|
return operation;
|
|
9909
10357
|
}));
|
|
@@ -9947,9 +10395,68 @@ class ConfigRepository {
|
|
|
9947
10395
|
return operation;
|
|
9948
10396
|
}));
|
|
9949
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
|
+
}
|
|
9950
10456
|
verify() {
|
|
9951
10457
|
verifyRepositoryIntegrity(this.db);
|
|
9952
10458
|
readRepositorySnapshot(this.db);
|
|
10459
|
+
this.getRuntimeRecoveryBudget();
|
|
9953
10460
|
}
|
|
9954
10461
|
claimController(controllerId, updatedAt) {
|
|
9955
10462
|
return runRepositoryAction(() => this.mutationTransaction(() => this.supervision.claim(controllerId, updatedAt)));
|
|
@@ -9982,6 +10489,9 @@ class ConfigRepository {
|
|
|
9982
10489
|
|
|
9983
10490
|
// src/config-storage/storage-protocol.ts
|
|
9984
10491
|
var STORAGE_METHODS = [
|
|
10492
|
+
"getPublicationRetirementRetry",
|
|
10493
|
+
"requestPublicationRetirementRetry",
|
|
10494
|
+
"freezePublicationRetirementDrainStart",
|
|
9985
10495
|
"commitPrepared",
|
|
9986
10496
|
"appendServingSnapshot",
|
|
9987
10497
|
"getServingSnapshot",
|
|
@@ -10005,9 +10515,26 @@ var STORAGE_METHODS = [
|
|
|
10005
10515
|
"recordWorkerResult",
|
|
10006
10516
|
"finalizePublication",
|
|
10007
10517
|
"markDraining",
|
|
10008
|
-
"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"
|
|
10009
10534
|
];
|
|
10010
10535
|
var READ_METHODS = new Set([
|
|
10536
|
+
"getPublicationRetirementRetry",
|
|
10537
|
+
"getPublicationServingSet",
|
|
10011
10538
|
"getServingSnapshot",
|
|
10012
10539
|
"getActivePublication",
|
|
10013
10540
|
"getOperation",
|
|
@@ -10016,7 +10543,12 @@ var READ_METHODS = new Set([
|
|
|
10016
10543
|
"getRecovery",
|
|
10017
10544
|
"getCurrentRecovery",
|
|
10018
10545
|
"getLatestRecovery",
|
|
10019
|
-
"verify"
|
|
10546
|
+
"verify",
|
|
10547
|
+
"getRetirementDescriptorCleanupState",
|
|
10548
|
+
"getRetiredWorkerInstanceIds",
|
|
10549
|
+
"getPublicationRetirement",
|
|
10550
|
+
"getRuntimeRecoveryBudget",
|
|
10551
|
+
"getRuntimeRecoveryStartupGate"
|
|
10020
10552
|
]);
|
|
10021
10553
|
function safeStorageError(error) {
|
|
10022
10554
|
const code = error instanceof ConfigRepositoryError ? error.code : "repository_failure";
|