@jeffusion/bungee-core 5.23.2 → 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.
@@ -1,61 +1,54 @@
1
1
  // @bun
2
- // src/config-storage/config-repository.ts
3
- import { Database as Database3 } from "bun:sqlite";
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
- // src/config-storage/repository-types.ts
8
- class ConfigRepositoryError extends Error {
9
- code;
10
- cause;
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
- function sqliteGet(db, sql, ...params) {
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/global-scalars.ts
40
- var LOG_LEVELS = new Set(["trace", "debug", "info", "warn", "error", "fatal"]);
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/persisted-validation.ts
185
- function corrupt(field) {
186
- throw new ConfigRepositoryError("schema_corrupt", `${field} contains an invalid persisted value`);
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
- function requireSafeInteger(value, field, minimum = 0) {
189
- if (!Number.isSafeInteger(value) || value < minimum)
190
- return corrupt(field);
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 requireBooleanInteger(value, field) {
194
- if (value !== 0 && value !== 1)
195
- return corrupt(field);
196
- return value === 1;
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
- function requirePositiveFinite(value, field) {
199
- if (!Number.isFinite(value) || value <= 0)
200
- return corrupt(field);
201
- return value;
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 requireUuid(value, field) {
204
- if (!isLowercaseUuid(value))
205
- return corrupt(field);
206
- return value;
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 requirePluginName(value, field) {
209
- if (!isPluginName(value))
210
- return corrupt(field);
211
- return value;
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 requireOptionalGlobalScalar(value, field) {
214
- if (value === null)
215
- return;
216
- const valid = field === "log_level" ? isLogLevel(value) : isBodyParserLimit(value);
217
- if (!valid)
218
- return corrupt(field);
219
- return value;
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
- function requireOneOf(value, allowed, field) {
222
- const match = allowed.find((candidate) => candidate === value);
223
- if (match === undefined)
224
- return corrupt(field);
225
- return match;
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
- return value;
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
- function preflightJsonGraph(input, context) {
6732
- let output;
6733
- const stack = [{ value: input, path: "", assign(value) {
6734
- output = value;
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 invalid(message) {
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
- invalid("recovery_id is invalid");
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
- invalid("recovery timestamp is invalid");
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
- invalid("automatic recovery retry timestamp would overflow");
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
- invalid("recovery attempt count is invalid");
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
- invalid("recovery reason detail is invalid");
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
- invalid("revision has not been superseded");
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
- invalid("recovery timestamp regressed");
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
- invalid("an active publication already exists");
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
- invalid("recovery timestamp regressed");
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
- invalid("recovery creation timestamp regressed");
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
- invalid("initial stopped recovery reason is invalid");
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
- invalid("recovery was not found");
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
- invalid("recovery was not found");
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
- invalid("next retry time must be in the future");
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
- invalid("recovery was not found");
8875
+ invalid2("recovery was not found");
8515
8876
  requireAttempt(attemptCount);
8516
- validateTerminalMatrix(state, attemptCount, finalReasonCode, invalid);
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
- invalid("recovery was not found");
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 invalid2(message) {
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
- invalid2("configuration aggregate is invalid");
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
- invalid2("configuration aggregate is too large");
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
- invalid2("serving snapshot identity is invalid");
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
- invalid2("configuration aggregate content hash does not match");
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
- invalid2("serving snapshot revision does not match configuration history");
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
- invalid2("serving snapshot lookup identity is invalid");
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(dirname(dbPath), { recursive: true });
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";