@camstack/addon-provider-onvif 1.2.125 → 1.2.126

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.
Files changed (3) hide show
  1. package/dist/addon.js +1631 -140
  2. package/dist/addon.mjs +1631 -140
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -5381,7 +5381,7 @@ var ZodIssueCode = {
5381
5381
  var ZodFirstPartyTypeKind;
5382
5382
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5383
5383
  //#endregion
5384
- //#region ../types/dist/sleep-DujUvOcb.mjs
5384
+ //#region ../types/dist/sleep-r3RWkGVx.mjs
5385
5385
  /**
5386
5386
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5387
5387
  * window to float samples (D455).
@@ -5498,7 +5498,7 @@ function isKeychainStoredField(field) {
5498
5498
  var SECRET_CLEAR = { $secretClear: true };
5499
5499
  /** The `'***'` token D634 redaction writes for non-admins. */
5500
5500
  var LEGACY_DEVICE_REDACTION = "***";
5501
- function isRecord$1(value) {
5501
+ function isRecord$2(value) {
5502
5502
  return typeof value === "object" && value !== null && !Array.isArray(value);
5503
5503
  }
5504
5504
  function makeSecretMarker(ref) {
@@ -5509,12 +5509,12 @@ function makeSecretMarker(ref) {
5509
5509
  }
5510
5510
  /** A stored marker: an object whose `$secret` is a non-empty string. */
5511
5511
  function isSecretMarker(value) {
5512
- if (!isRecord$1(value)) return false;
5512
+ if (!isRecord$2(value)) return false;
5513
5513
  const ref = value["$secret"];
5514
5514
  return typeof ref === "string" && ref.length > 0;
5515
5515
  }
5516
5516
  function isSecretClear(value) {
5517
- return isRecord$1(value) && value["$secretClear"] === true;
5517
+ return isRecord$2(value) && value["$secretClear"] === true;
5518
5518
  }
5519
5519
  /**
5520
5520
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -6030,6 +6030,13 @@ function globalSecretScope() {
6030
6030
  function encodeSegment(value) {
6031
6031
  return encodeURIComponent(value);
6032
6032
  }
6033
+ function decodeSegment(value) {
6034
+ try {
6035
+ return decodeURIComponent(value);
6036
+ } catch {
6037
+ return null;
6038
+ }
6039
+ }
6033
6040
  /**
6034
6041
  * The field path of a value inside an array item, addressed by the item's id:
6035
6042
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6038,6 +6045,75 @@ function encodeSegment(value) {
6038
6045
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6039
6046
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6040
6047
  }
6048
+ function parseScope(segments) {
6049
+ switch (segments[0]) {
6050
+ case "global": return {
6051
+ scope: GLOBAL_SCOPE,
6052
+ used: 1
6053
+ };
6054
+ case "node": {
6055
+ const nodeId = segments[1] !== void 0 ? decodeSegment(segments[1]) : null;
6056
+ return nodeId === null || nodeId.length === 0 ? null : {
6057
+ scope: {
6058
+ kind: "node",
6059
+ nodeId
6060
+ },
6061
+ used: 2
6062
+ };
6063
+ }
6064
+ case "device": {
6065
+ const raw = segments[1];
6066
+ if (raw === void 0 || !/^\d+$/.test(raw)) return null;
6067
+ return {
6068
+ scope: {
6069
+ kind: "device",
6070
+ deviceId: Number(raw)
6071
+ },
6072
+ used: 2
6073
+ };
6074
+ }
6075
+ case "integration": {
6076
+ const id = segments[1] !== void 0 ? decodeSegment(segments[1]) : null;
6077
+ return id === null || id.length === 0 ? null : {
6078
+ scope: {
6079
+ kind: "integration",
6080
+ integrationId: id
6081
+ },
6082
+ used: 2
6083
+ };
6084
+ }
6085
+ case "row": {
6086
+ const collection = segments[1] !== void 0 ? decodeSegment(segments[1]) : null;
6087
+ const rowId = segments[2] !== void 0 ? decodeSegment(segments[2]) : null;
6088
+ if (collection === null || rowId === null || collection.length === 0 || rowId.length === 0) return null;
6089
+ return {
6090
+ scope: {
6091
+ kind: "row",
6092
+ collection,
6093
+ rowId
6094
+ },
6095
+ used: 3
6096
+ };
6097
+ }
6098
+ default: return null;
6099
+ }
6100
+ }
6101
+ /** Take a ref apart; `null` when it is not a well-formed ref. */
6102
+ function parseSecretRef(ref) {
6103
+ if (!ref.startsWith("secret://")) return null;
6104
+ const segments = ref.slice(9).split("/");
6105
+ const owner = segments[0] !== void 0 ? decodeSegment(segments[0]) : null;
6106
+ if (owner === null || owner.length === 0) return null;
6107
+ const parsed = parseScope(segments.slice(1));
6108
+ if (parsed === null) return null;
6109
+ const fieldPath = segments.slice(1 + parsed.used).join("/");
6110
+ if (fieldPath.length === 0) return null;
6111
+ return {
6112
+ owner,
6113
+ scope: parsed.scope,
6114
+ fieldPath
6115
+ };
6116
+ }
6041
6117
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
6042
6118
  var KeychainWriteRefusedError = class extends Error {
6043
6119
  fieldPath;
@@ -6048,7 +6124,7 @@ var KeychainWriteRefusedError = class extends Error {
6048
6124
  }
6049
6125
  };
6050
6126
  var KEEP = { kind: "keep" };
6051
- function isRecord(value) {
6127
+ function isRecord$1(value) {
6052
6128
  return typeof value === "object" && value !== null && !Array.isArray(value);
6053
6129
  }
6054
6130
  function defaultMintId() {
@@ -6061,7 +6137,7 @@ function collectSecretMarkerRefs(value, out = []) {
6061
6137
  return out;
6062
6138
  }
6063
6139
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6064
- else if (isRecord(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6140
+ else if (isRecord$1(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6065
6141
  return out;
6066
6142
  }
6067
6143
  async function storedValueEquals(secrets, marker, candidate) {
@@ -6116,7 +6192,7 @@ function rowId(row, idKey) {
6116
6192
  return null;
6117
6193
  }
6118
6194
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
6119
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord) : [];
6195
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$1) : [];
6120
6196
  if (!Array.isArray(incoming)) {
6121
6197
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6122
6198
  return {
@@ -6132,7 +6208,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6132
6208
  const kept = /* @__PURE__ */ new Set();
6133
6209
  const rows = [];
6134
6210
  for (const row of incoming) {
6135
- if (!isRecord(row)) {
6211
+ if (!isRecord$1(row)) {
6136
6212
  rows.push(row);
6137
6213
  continue;
6138
6214
  }
@@ -6157,7 +6233,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6157
6233
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
6158
6234
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
6159
6235
  case "object": {
6160
- if (!isRecord(incoming)) {
6236
+ if (!isRecord$1(incoming)) {
6161
6237
  if (incoming === void 0) return KEEP;
6162
6238
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
6163
6239
  return {
@@ -6165,7 +6241,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6165
6241
  value: incoming
6166
6242
  };
6167
6243
  }
6168
- const storedObj = isRecord(stored) ? stored : {};
6244
+ const storedObj = isRecord$1(stored) ? stored : {};
6169
6245
  return {
6170
6246
  kind: "value",
6171
6247
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -6224,12 +6300,19 @@ async function transformSecretWrite(input) {
6224
6300
  }
6225
6301
  };
6226
6302
  }
6303
+ /** Is `ref` one of `owner`'s refs? */
6304
+ function isSecretRefOwnedBy(ref, owner) {
6305
+ return parseSecretRef(ref)?.owner === owner;
6306
+ }
6227
6307
  /**
6228
6308
  * Replace every marker in `value` with its keychain value. Returns the SAME
6229
6309
  * reference when there is no marker (no allocation, no keychain call).
6230
6310
  */
6231
- async function resolveSecretMarkers(value, secrets) {
6232
- const refs = collectSecretMarkerRefs(value);
6311
+ async function resolveSecretMarkers(value, secrets, options = {}) {
6312
+ const keepMarker = options.unresolved === "keep-marker";
6313
+ const owner = options.ownedBy;
6314
+ const inScope = (ref) => owner === void 0 || isSecretRefOwnedBy(ref, owner);
6315
+ const refs = collectSecretMarkerRefs(value).filter(inScope);
6233
6316
  if (refs.length === 0) return {
6234
6317
  value,
6235
6318
  unresolved: []
@@ -6253,10 +6336,18 @@ async function resolveSecretMarkers(value, secrets) {
6253
6336
  }
6254
6337
  const substitute = (v) => {
6255
6338
  if (isSecretMarker(v)) {
6339
+ if (!inScope(v.$secret)) return {
6340
+ keep: true,
6341
+ value: v
6342
+ };
6256
6343
  const answer = answers[v.$secret];
6257
- return answer?.kind === "value" ? {
6344
+ if (answer?.kind === "value") return {
6258
6345
  keep: true,
6259
6346
  value: answer.value
6347
+ };
6348
+ return keepMarker ? {
6349
+ keep: true,
6350
+ value: v
6260
6351
  } : {
6261
6352
  keep: false,
6262
6353
  value: void 0
@@ -6273,7 +6364,7 @@ async function resolveSecretMarkers(value, secrets) {
6273
6364
  value: items
6274
6365
  };
6275
6366
  }
6276
- if (isRecord(v)) {
6367
+ if (isRecord$1(v)) {
6277
6368
  const out = {};
6278
6369
  for (const [k, child] of Object.entries(v)) {
6279
6370
  const s = substitute(child);
@@ -6294,14 +6385,152 @@ async function resolveSecretMarkers(value, secrets) {
6294
6385
  unresolved
6295
6386
  };
6296
6387
  }
6297
- function touchesTree(tree, patch) {
6298
- if (tree.size === 0) return false;
6299
- for (const key of Object.keys(patch)) {
6300
- const at = key.indexOf("@");
6301
- if (tree.has(at > 0 ? key.slice(0, at) : key)) return true;
6388
+ /** An addon tried to replace a stored secret it could not read. Nothing was written. */
6389
+ var SecretOverwriteRefusedError = class extends Error {
6390
+ ref;
6391
+ reason;
6392
+ constructor(ref, reason) {
6393
+ super(`secret ${ref} was not replaced: its stored value cannot be read right now (${reason}), and an addon may not overwrite a secret it cannot see. Nothing was written.`);
6394
+ this.ref = ref;
6395
+ this.reason = reason;
6396
+ this.name = "SecretOverwriteRefusedError";
6397
+ }
6398
+ };
6399
+ function isRecord(value) {
6400
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6401
+ }
6402
+ function resolvedValue(pass, marker) {
6403
+ const answer = pass.answers[marker.$secret];
6404
+ return answer?.kind === "value" ? answer.value : null;
6405
+ }
6406
+ async function atMarker(pass, incoming, stored) {
6407
+ if (incoming === null) return stored;
6408
+ if (typeof incoming !== "string") return incoming;
6409
+ if (incoming === resolvedValue(pass, stored)) return stored;
6410
+ const intent = classifySecretWrite(incoming);
6411
+ if (intent.kind !== "set") return stored;
6412
+ const ref = stored.$secret;
6413
+ if (!isSecretRefOwnedBy(ref, pass.secrets.owner)) return incoming;
6414
+ const answer = pass.answers[ref];
6415
+ if ((answer === void 0 || answer.kind === "unavailable" || answer.kind === "forbidden") && !pass.operatorWrite) throw new SecretOverwriteRefusedError(ref, answer === void 0 ? "not-initialized" : answer.kind === "unavailable" ? answer.reason : answer.kind);
6416
+ try {
6417
+ await pass.secrets.put(ref, intent.value);
6418
+ } catch (err) {
6419
+ throw new KeychainWriteRefusedError(ref, err);
6420
+ }
6421
+ pass.changedRefs.push(ref);
6422
+ return stored;
6423
+ }
6424
+ function inArray(pass, incoming, stored) {
6425
+ return incoming.map((item, index) => {
6426
+ const twin = stored[index];
6427
+ if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
6428
+ if (isRecord(item) && isRecord(twin)) return inArrayRow(pass, item, twin);
6429
+ if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
6430
+ return item;
6431
+ });
6432
+ }
6433
+ function inArrayRow(pass, incoming, stored) {
6434
+ const out = { ...incoming };
6435
+ for (const [key, value] of Object.entries(incoming)) {
6436
+ const twin = stored[key];
6437
+ if (isSecretMarker(twin)) {
6438
+ if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
6439
+ } else if (isRecord(value) && isRecord(twin)) out[key] = inArrayRow(pass, value, twin);
6440
+ else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
6441
+ }
6442
+ return out;
6443
+ }
6444
+ async function walk(pass, incoming, stored) {
6445
+ if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
6446
+ if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
6447
+ if (!isRecord(incoming) || !isRecord(stored)) return incoming;
6448
+ const out = { ...incoming };
6449
+ for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
6450
+ return out;
6451
+ }
6452
+ /** Restore markers a write-back carries as values. See the file doc. */
6453
+ async function restoreUnchangedSecrets(input) {
6454
+ const refs = [];
6455
+ for (const key of Object.keys(input.patch)) collectSecretMarkerRefs(input.stored[key], refs);
6456
+ const owned = refs.filter((ref) => isSecretRefOwnedBy(ref, input.secrets.owner));
6457
+ if (owned.length === 0) return {
6458
+ patch: input.patch,
6459
+ changedRefs: []
6460
+ };
6461
+ const pass = {
6462
+ secrets: input.secrets,
6463
+ answers: await input.secrets.resolve([...new Set(owned)]),
6464
+ operatorWrite: input.operatorWrite,
6465
+ changedRefs: []
6466
+ };
6467
+ const out = {};
6468
+ for (const [key, value] of Object.entries(input.patch)) out[key] = await walk(pass, value, input.stored[key]);
6469
+ return {
6470
+ patch: out,
6471
+ changedRefs: pass.changedRefs
6472
+ };
6473
+ }
6474
+ /** Structural equality over JSON-shaped values (key order does not matter). */
6475
+ function isJsonEqual(a, b) {
6476
+ if (Object.is(a, b)) return true;
6477
+ if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
6478
+ if (isRecord(a) && isRecord(b)) {
6479
+ const keys = Object.keys(a);
6480
+ if (keys.length !== Object.keys(b).length) return false;
6481
+ return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
6302
6482
  }
6303
6483
  return false;
6304
6484
  }
6485
+ /** The patch without the keys whose value already equals the stored one. */
6486
+ function dropUnchangedKeys(patch, stored) {
6487
+ const out = {};
6488
+ for (const [key, value] of Object.entries(patch)) {
6489
+ if (Object.prototype.hasOwnProperty.call(stored, key) && isJsonEqual(value, stored[key])) continue;
6490
+ out[key] = value;
6491
+ }
6492
+ return out;
6493
+ }
6494
+ /** The top-level keys of a blob that hold at least one of `owner`'s markers. */
6495
+ function markerKeysOf(blob, owner) {
6496
+ const keys = /* @__PURE__ */ new Set();
6497
+ for (const [key, value] of Object.entries(blob)) if (collectSecretMarkerRefs(value).some((ref) => isSecretRefOwnedBy(ref, owner))) keys.add(key);
6498
+ return keys;
6499
+ }
6500
+ function nodeFromStored(value, owner) {
6501
+ if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
6502
+ if (!isRecord(value)) return null;
6503
+ const children = /* @__PURE__ */ new Map();
6504
+ for (const [key, child] of Object.entries(value)) {
6505
+ const node = nodeFromStored(child, owner);
6506
+ if (node !== null) children.set(key, node);
6507
+ }
6508
+ return children.size > 0 ? {
6509
+ kind: "object",
6510
+ children
6511
+ } : null;
6512
+ }
6513
+ /**
6514
+ * The declared secret tree, widened by every place the STORED blob holds a
6515
+ * marker under one of `keys` — so a marker the schema no longer declares (or
6516
+ * never declared: Wyze before b4906cec6) still gets the save path's
6517
+ * KEEP / SET / CLEAR semantics instead of being overwritten by whatever the
6518
+ * addon writes. Markers inside arrays are not added: a row has no id field to
6519
+ * match by unless the schema declares one. Only `owner`'s markers count: a
6520
+ * builtin that keeps another addon's marker (device-manager) never takes it
6521
+ * over.
6522
+ */
6523
+ function widenTreeWithStoredMarkers(declared, stored, keys, owner) {
6524
+ const added = /* @__PURE__ */ new Map();
6525
+ for (const key of keys) {
6526
+ const at = key.indexOf("@");
6527
+ const base = at > 0 ? key.slice(0, at) : key;
6528
+ if (declared.has(base) || added.has(base)) continue;
6529
+ const node = nodeFromStored(stored[key], owner);
6530
+ if (node !== null) added.set(base, node);
6531
+ }
6532
+ return added.size === 0 ? declared : new Map([...declared, ...added]);
6533
+ }
6305
6534
  /** The blob as stored now; a failed read REFUSES the write instead of guessing. */
6306
6535
  async function readStoredAddonBlob(raw) {
6307
6536
  const threeWay = raw.readAddonStoreResult;
@@ -6310,7 +6539,41 @@ async function readStoredAddonBlob(raw) {
6310
6539
  if (result.kind === "failed") throw new Error(`settings write refused: the stored blob could not be read (${result.error})`);
6311
6540
  return result.kind === "missing" ? {} : result.value;
6312
6541
  }
6313
- function createSecretAwareSettings(raw, opts) {
6542
+ function touchesTree(tree, patch) {
6543
+ if (tree.size === 0) return false;
6544
+ for (const key of Object.keys(patch)) {
6545
+ const at = key.indexOf("@");
6546
+ if (tree.has(at > 0 ? key.slice(0, at) : key)) return true;
6547
+ }
6548
+ return false;
6549
+ }
6550
+ /**
6551
+ * Which top-level keys of each store held markers when last seen. `undefined`
6552
+ * for a store not seen yet: its first write reads it, to be safe.
6553
+ */
6554
+ var MarkerKeyIndex = class {
6555
+ owner;
6556
+ byStore = /* @__PURE__ */ new Map();
6557
+ constructor(owner) {
6558
+ this.owner = owner;
6559
+ }
6560
+ seen(store, blob) {
6561
+ this.byStore.set(store, markerKeysOf(blob, this.owner));
6562
+ }
6563
+ wrote(store, patch) {
6564
+ const keys = this.byStore.get(store);
6565
+ if (keys === void 0) return;
6566
+ const now = markerKeysOf(patch, this.owner);
6567
+ for (const key of Object.keys(patch)) if (now.has(key)) keys.add(key);
6568
+ else keys.delete(key);
6569
+ }
6570
+ mayHoldMarkers(store, patch) {
6571
+ const keys = this.byStore.get(store);
6572
+ if (keys === void 0) return true;
6573
+ return Object.keys(patch).some((key) => keys.has(key));
6574
+ }
6575
+ };
6576
+ function createHandle(raw, opts) {
6314
6577
  const chains = /* @__PURE__ */ new Map();
6315
6578
  const serialize = (key, fn) => {
6316
6579
  const next = (chains.get(key) ?? Promise.resolve()).then(fn, fn);
@@ -6321,46 +6584,130 @@ function createSecretAwareSettings(raw, opts) {
6321
6584
  });
6322
6585
  return next;
6323
6586
  };
6324
- const write = async (chainKey, tree, scope, patch, readStored, rawWrite) => {
6325
- if (!touchesTree(tree, patch)) return rawWrite(patch);
6326
- return serialize(chainKey, async () => {
6587
+ const index = new MarkerKeyIndex(opts.secrets.owner);
6588
+ const resolveRead = async (store, blob) => {
6589
+ index.seen(store, blob);
6590
+ const { value, unresolved } = await resolveSecretMarkers(blob, opts.secrets, {
6591
+ unresolved: "keep-marker",
6592
+ ownedBy: opts.secrets.owner
6593
+ });
6594
+ if (unresolved.length > 0) opts.onUnresolved?.(unresolved);
6595
+ return value;
6596
+ };
6597
+ const write = async (store, declared, scope, patch, readStored, rawWrite) => {
6598
+ if (!touchesTree(declared, patch) && !index.mayHoldMarkers(store, patch)) {
6599
+ await rawWrite(patch);
6600
+ index.wrote(store, patch);
6601
+ return { changed: true };
6602
+ }
6603
+ return serialize(store, async () => {
6327
6604
  const stored = await readStored();
6605
+ index.seen(store, stored);
6606
+ const operatorWrite = opts.isOperatorWrite?.() === true;
6607
+ const restored = await restoreUnchangedSecrets({
6608
+ patch,
6609
+ stored,
6610
+ secrets: opts.secrets,
6611
+ operatorWrite
6612
+ });
6328
6613
  const result = await transformSecretWrite({
6329
- tree,
6614
+ tree: widenTreeWithStoredMarkers(declared, stored, Object.keys(restored.patch), opts.secrets.owner),
6330
6615
  secrets: opts.secrets,
6331
- patch,
6616
+ patch: restored.patch,
6332
6617
  stored,
6333
6618
  ...scope !== void 0 ? { scope } : {},
6334
- operatorWrite: opts.isOperatorWrite?.() === true
6619
+ operatorWrite
6335
6620
  });
6336
- if (Object.keys(result.patch).length > 0) await rawWrite(result.patch);
6621
+ const effective = dropUnchangedKeys(result.patch, stored);
6622
+ const wroteBlob = Object.keys(effective).length > 0;
6623
+ if (wroteBlob) {
6624
+ await rawWrite(effective);
6625
+ index.wrote(store, effective);
6626
+ }
6337
6627
  const { orphaned } = await result.afterWrite();
6338
6628
  if (orphaned.length > 0) opts.onOrphaned(orphaned);
6629
+ return { changed: wroteBlob || restored.changedRefs.length > 0 };
6339
6630
  });
6340
6631
  };
6341
- const wrapped = {
6342
- writeAddonStore: (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p)),
6343
- writeDeviceStore: (deviceId, patch) => write(`device:${deviceId}`, opts.deviceTree(), {
6344
- kind: "device",
6345
- deviceId
6346
- }, patch, () => raw.readDeviceStore(deviceId), (p) => raw.writeDeviceStore(deviceId, p))
6632
+ const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
6633
+ const readAddonStoreResult = raw.readAddonStoreResult;
6634
+ const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
6635
+ const readDeviceStoreBatch = raw.readDeviceStoreBatch;
6636
+ const overrides = {
6637
+ readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
6638
+ ...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
6639
+ const result = await readAddonStoreResult.call(raw);
6640
+ return result.kind === "value" ? {
6641
+ kind: "value",
6642
+ value: await resolveRead("addon", result.value)
6643
+ } : result;
6644
+ } } : {},
6645
+ ...readAddonStoreKeyResult !== void 0 ? { readAddonStoreKeyResult: async (key) => {
6646
+ const result = await readAddonStoreKeyResult.call(raw, key);
6647
+ if (result.kind !== "present") return result;
6648
+ const { value, unresolved } = await resolveSecretMarkers({ [key]: result.value }, opts.secrets, {
6649
+ unresolved: "keep-marker",
6650
+ ownedBy: opts.secrets.owner
6651
+ });
6652
+ if (unresolved.length > 0) opts.onUnresolved?.(unresolved);
6653
+ return {
6654
+ kind: "present",
6655
+ value: value[key]
6656
+ };
6657
+ } } : {},
6658
+ readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
6659
+ ...readDeviceStoreBatch !== void 0 ? { readDeviceStoreBatch: async (deviceIds) => {
6660
+ const rows = await readDeviceStoreBatch.call(raw, deviceIds);
6661
+ const out = /* @__PURE__ */ new Map();
6662
+ for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
6663
+ return out;
6664
+ } } : {},
6665
+ writeAddonStore: async (patch) => {
6666
+ await writeAddon(patch);
6667
+ },
6668
+ writeDeviceStore: async (deviceId, patch) => {
6669
+ await write(`device:${deviceId}`, opts.deviceTree(), {
6670
+ kind: "device",
6671
+ deviceId
6672
+ }, patch, () => raw.readDeviceStore(deviceId), (p) => raw.writeDeviceStore(deviceId, p));
6673
+ }
6674
+ };
6675
+ return {
6676
+ settings: new Proxy(raw, { get(target, prop, receiver) {
6677
+ if (typeof prop === "string" && Object.prototype.hasOwnProperty.call(overrides, prop)) return Reflect.get(overrides, prop);
6678
+ const value = Reflect.get(target, prop, receiver);
6679
+ return typeof value === "function" ? value.bind(target) : value;
6680
+ } }),
6681
+ raw: {
6682
+ readAddonStore: async () => {
6683
+ const blob = await raw.readAddonStore();
6684
+ index.seen("addon", blob);
6685
+ return blob;
6686
+ },
6687
+ readDeviceStore: async (deviceId) => {
6688
+ const blob = await raw.readDeviceStore(deviceId);
6689
+ index.seen(`device:${deviceId}`, blob);
6690
+ return blob;
6691
+ }
6692
+ },
6693
+ writeAddonStore: writeAddon
6347
6694
  };
6348
- return new Proxy(raw, { get(target, prop, receiver) {
6349
- if (prop === "writeAddonStore") return wrapped.writeAddonStore;
6350
- if (prop === "writeDeviceStore") return wrapped.writeDeviceStore;
6351
- const value = Reflect.get(target, prop, receiver);
6352
- return typeof value === "function" ? value.bind(target) : value;
6353
- } });
6354
6695
  }
6355
- /** The context the addon sees: identical, except that its settings writes are secret-aware. */
6356
- function withSecretAwareSettings(context, opts) {
6696
+ /** The context the addon sees, plus the private raw view and write outcome. */
6697
+ function createSecretAwareContext(context, opts) {
6357
6698
  const raw = context.settings;
6358
- if (raw === void 0) return context;
6359
- const settings = createSecretAwareSettings(raw, opts);
6360
- return new Proxy(context, { get(target, prop, receiver) {
6361
- if (prop === "settings") return settings;
6362
- return Reflect.get(target, prop, receiver);
6363
- } });
6699
+ if (raw === void 0) return {
6700
+ context,
6701
+ handle: null
6702
+ };
6703
+ const handle = createHandle(raw, opts);
6704
+ return {
6705
+ context: new Proxy(context, { get(target, prop, receiver) {
6706
+ if (prop === "settings") return handle.settings;
6707
+ return Reflect.get(target, prop, receiver);
6708
+ } }),
6709
+ handle
6710
+ };
6364
6711
  }
6365
6712
  var LEAF = { kind: "leaf" };
6366
6713
  var DEFAULT_ITEM_ID_KEY = "id";
@@ -6535,6 +6882,14 @@ var BaseAddon = class {
6535
6882
  _secretChangeUnsub = null;
6536
6883
  /** >0 while an OPERATOR's settings write is being applied. */
6537
6884
  _operatorWriteDepth = 0;
6885
+ /**
6886
+ * The keychain-aware settings handle (D798). Its `raw` view is the ONLY way
6887
+ * to the marker-bearing blobs and stays private to this class: the form
6888
+ * hydrate and config resolution need markers, addon code never does.
6889
+ */
6890
+ _secretSettings = null;
6891
+ /** Refs an addon-facing read could not resolve — each is logged once per process. */
6892
+ _loggedUnresolvedReads = /* @__PURE__ */ new Set();
6538
6893
  constructor(defaults) {
6539
6894
  this.defaults = defaults;
6540
6895
  this._config = { ...defaults };
@@ -6571,13 +6926,21 @@ var BaseAddon = class {
6571
6926
  }
6572
6927
  async initialize(context) {
6573
6928
  const secrets = context.secrets;
6574
- this._ctx = secrets !== void 0 ? withSecretAwareSettings(context, {
6575
- secrets,
6576
- addonTree: () => this.addonSecretTree(),
6577
- deviceTree: () => this.deviceSecretTree(),
6578
- onOrphaned: (refs) => context.logger.warn("keychain refs dropped by a settings write could not be deleted", { meta: { refs: [...refs] } }),
6579
- isOperatorWrite: () => this._operatorWriteDepth > 0
6580
- }) : context;
6929
+ if (secrets !== void 0) {
6930
+ const { context: wrapped, handle } = createSecretAwareContext(context, {
6931
+ secrets,
6932
+ addonTree: () => this.addonSecretTree(),
6933
+ deviceTree: () => this.deviceSecretTree(),
6934
+ onOrphaned: (refs) => context.logger.warn("keychain refs dropped by a settings write could not be deleted", { meta: { refs: [...refs] } }),
6935
+ isOperatorWrite: () => this._operatorWriteDepth > 0,
6936
+ onUnresolved: (unresolved) => this.logUnresolvedRead(unresolved)
6937
+ });
6938
+ this._ctx = wrapped;
6939
+ this._secretSettings = handle;
6940
+ } else {
6941
+ this._ctx = context;
6942
+ this._secretSettings = null;
6943
+ }
6581
6944
  await this.resolveConfig();
6582
6945
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
6583
6946
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -6694,11 +7057,8 @@ var BaseAddon = class {
6694
7057
  async getGlobalSettings(overlay, cap, nodeId) {
6695
7058
  const schema = this.globalSettingsSchema(cap);
6696
7059
  if (!schema) return { sections: [] };
6697
- const projected = await this.resolveGlobalStore(nodeId, cap);
6698
- return hydrateSchema(schema, overlay ? {
6699
- ...projected,
6700
- ...overlay
6701
- } : projected, { inlineSecretsAsViews: true });
7060
+ const projected = await this.readRawProjectedStore(nodeId, cap);
7061
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
6702
7062
  }
6703
7063
  /**
6704
7064
  * The integration-level view of this addon's settings: exactly the sections
@@ -6743,7 +7103,7 @@ var BaseAddon = class {
6743
7103
  });
6744
7104
  }
6745
7105
  if (sections.length === 0) return null;
6746
- const projected = await this.resolveGlobalStore(nodeId);
7106
+ const projected = await this.readRawProjectedStore(nodeId);
6747
7107
  return hydrateSchema({
6748
7108
  ...schema,
6749
7109
  sections
@@ -6760,12 +7120,50 @@ var BaseAddon = class {
6760
7120
  * the store for custom option logic (option narrowing, value snapping) to
6761
7121
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
6762
7122
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
7123
+ *
7124
+ * Keychain values come back RESOLVED (D798): a secret reads as its value,
7125
+ * never as a `{ $secret }` marker. One the keychain cannot serve right now
7126
+ * stays a marker — "stored, unavailable" (`isSecretMarker()` is true, and it
7127
+ * is listed in {@link unresolvedSecrets}) — never `''` and never absent, so
7128
+ * it cannot be mistaken for "nothing stored, mint a new one". Writing the
7129
+ * resolved blob back is a no-op for every unchanged secret.
6763
7130
  */
6764
7131
  async resolveGlobalStore(nodeId, cap) {
6765
7132
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
7133
+ return this.projectForNode(raw, nodeId, cap);
7134
+ }
7135
+ projectForNode(store, nodeId, cap) {
6766
7136
  const keys = this.perNodeKeys(cap);
6767
7137
  const node = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
6768
- return keys.size > 0 ? projectStore(raw, keys, node) : raw;
7138
+ return keys.size > 0 ? projectStore(store, keys, node) : store;
7139
+ }
7140
+ /** The PROJECTED store with its markers — the form hydrate only (D798). */
7141
+ async readRawProjectedStore(nodeId, cap) {
7142
+ const raw = await this.rawSettings()?.readAddonStore() ?? {};
7143
+ return this.projectForNode(raw, nodeId, cap);
7144
+ }
7145
+ /**
7146
+ * `{ ...stored, ...overlay }`, except that an overlay member equal to the
7147
+ * RESOLVED stored one keeps the stored (marker-bearing) member — so a form
7148
+ * hydrated from a resolved overlay still shows a secret as "set", never as
7149
+ * its value.
7150
+ */
7151
+ async overlayKeepingMarkers(stored, overlay) {
7152
+ const secrets = this._ctx?.secrets;
7153
+ const merged = {
7154
+ ...stored,
7155
+ ...overlay
7156
+ };
7157
+ if (secrets === void 0 || collectSecretMarkerRefs(stored).length === 0) return merged;
7158
+ const { value: resolved } = await resolveSecretMarkers(stored, secrets, {
7159
+ unresolved: "keep-marker",
7160
+ ownedBy: secrets.owner
7161
+ });
7162
+ for (const key of Object.keys(overlay)) {
7163
+ if (collectSecretMarkerRefs(stored[key]).length === 0) continue;
7164
+ if (isJsonEqual(overlay[key], resolved[key])) merged[key] = stored[key];
7165
+ }
7166
+ return merged;
6769
7167
  }
6770
7168
  /**
6771
7169
  * Whether ONE global key of this addon's store was written, read three-way
@@ -6830,8 +7228,12 @@ var BaseAddon = class {
6830
7228
  async clearSecrets(keys, nodeId) {
6831
7229
  const tree = this.addonSecretTree();
6832
7230
  const keychain = this._ctx?.secrets !== void 0;
7231
+ const stored = keychain ? await this.readAddonStoreWithRetry() : {};
6833
7232
  const patch = {};
6834
- for (const key of keys) patch[key] = keychain && tree.has(key) ? SECRET_CLEAR : "";
7233
+ for (const key of keys) {
7234
+ const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
7235
+ patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
7236
+ }
6835
7237
  await this.applyGlobalPatch(patch, nodeId);
6836
7238
  }
6837
7239
  async applyGlobalPatch(barePatch, nodeId) {
@@ -6839,7 +7241,11 @@ var BaseAddon = class {
6839
7241
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
6840
7242
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
6841
7243
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
6842
- await this._ctx?.settings?.writeAddonStore(scoped);
7244
+ const handle = this._secretSettings;
7245
+ if (handle !== null) {
7246
+ const { changed } = await handle.writeAddonStore(scoped);
7247
+ if (!changed) return;
7248
+ } else await this._ctx?.settings?.writeAddonStore(scoped);
6843
7249
  if (target !== localNode) return;
6844
7250
  await this.resolveConfig();
6845
7251
  await this.onConfigChanged();
@@ -6892,7 +7298,7 @@ var BaseAddon = class {
6892
7298
  async getDeviceSettings(deviceId) {
6893
7299
  const schema = this.deviceSettingsSchema();
6894
7300
  if (!schema) return { sections: [] };
6895
- return hydrateSchema(schema, await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, { inlineSecretsAsViews: true });
7301
+ return hydrateSchema(schema, await this.rawSettings()?.readDeviceStore(deviceId) ?? {}, { inlineSecretsAsViews: true });
6896
7302
  }
6897
7303
  async updateDeviceSettings(deviceId, patch) {
6898
7304
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
@@ -7064,7 +7470,7 @@ var BaseAddon = class {
7064
7470
  key,
7065
7471
  schema,
7066
7472
  fallback,
7067
- read: async () => this.resolveStoredSecrets(await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, false),
7473
+ read: async () => this.resolveStoredSecrets(await this.rawSettings()?.readDeviceStore(deviceId) ?? {}, false),
7068
7474
  write: async (patch) => {
7069
7475
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
7070
7476
  },
@@ -7126,6 +7532,23 @@ var BaseAddon = class {
7126
7532
  }
7127
7533
  return value;
7128
7534
  }
7535
+ /**
7536
+ * The marker-bearing settings reads (D798) — private on purpose. Without a
7537
+ * keychain the context's own view is already raw.
7538
+ */
7539
+ rawSettings() {
7540
+ return this._secretSettings?.raw ?? this._ctx?.settings;
7541
+ }
7542
+ /** An addon-facing read met markers it could not resolve: say so once per ref. */
7543
+ logUnresolvedRead(unresolved) {
7544
+ const fresh = unresolved.filter((u) => !this._loggedUnresolvedReads.has(u.ref));
7545
+ if (fresh.length === 0) return;
7546
+ for (const u of fresh) this._loggedUnresolvedReads.add(u.ref);
7547
+ this._ctx?.logger?.warn?.("settings read: a stored secret is unavailable — it reads as its marker, not as a value", { meta: { secrets: fresh.map((u) => ({
7548
+ ref: u.ref,
7549
+ reason: u.reason
7550
+ })) } });
7551
+ }
7129
7552
  /** Re-resolve the config (a keychain-only change, or a retry) and apply it. */
7130
7553
  scheduleSecretReresolve(delayMs) {
7131
7554
  const timerOf = delayMs === SECRET_CHANGE_DEBOUNCE_MS ? "_secretChangeTimer" : "_secretRetryTimer";
@@ -7152,7 +7575,7 @@ var BaseAddon = class {
7152
7575
  * `{}` (defaults) — the addon's first successful patch will rehydrate.
7153
7576
  */
7154
7577
  async readAddonStoreWithRetry() {
7155
- const settings = this._ctx?.settings;
7578
+ const settings = this.rawSettings();
7156
7579
  if (!settings) return {};
7157
7580
  const delaysMs = [
7158
7581
  150,
@@ -9388,6 +9811,278 @@ var CameraSwitchGroupSchema = object({
9388
9811
  fetchedAt: number()
9389
9812
  });
9390
9813
  /**
9814
+ * The trainer contract — what the hub hands the cloud trainer and what it
9815
+ * gets back (cloud training, D805+).
9816
+ *
9817
+ * The trainer (`hf/camstack-trainer`, published as the Hugging Face repo
9818
+ * `camstack/camstack-trainer`) is a separate AGPL program the hub never
9819
+ * imports: `job.json` goes in, files come out, and progress is `CAMSTACK_EVENT`
9820
+ * lines on stdout. These schemas are the hub's half of that boundary; the
9821
+ * trainer's pydantic models (and the JSON Schemas generated from them under
9822
+ * `hf/camstack-trainer/schemas/`) are the other half. A change here is a
9823
+ * change there, in the same piece of work.
9824
+ *
9825
+ * No `.default()` anywhere: defaults do not run on the addon cap path, so the
9826
+ * orchestrator fills every field of a job spec explicitly.
9827
+ */
9828
+ var TRAINING_JOB_SCHEMA = "camstack.training/v1";
9829
+ var TRAINING_RESULT_SCHEMA = "camstack.training-result/v1";
9830
+ /** What the trainer can export. Edge TPU is a later, separate image. */
9831
+ var TrainingExportSchema = _enum([
9832
+ "onnx",
9833
+ "openvino-fp16",
9834
+ "openvino-int8",
9835
+ "coreml-fp16"
9836
+ ]);
9837
+ var FloorsSchema = record(string(), number().min(0).max(1));
9838
+ object({
9839
+ schema: literal(TRAINING_JOB_SCHEMA),
9840
+ jobKey: string().uuid(),
9841
+ target: object({
9842
+ kind: literal("detector"),
9843
+ step: literal("object-detection")
9844
+ }),
9845
+ base: object({
9846
+ modelId: string().min(1),
9847
+ /** Ultralytics weights id, e.g. `yolov9m.pt`. */
9848
+ weights: string().min(1),
9849
+ imgsz: number().int().positive()
9850
+ }),
9851
+ baseline: object({
9852
+ modelId: string().min(1),
9853
+ /** The baseline ONNX the holdout comparison runs; public mirror URL. */
9854
+ onnxUrl: string().url(),
9855
+ imgsz: number().int().positive(),
9856
+ floors: FloorsSchema
9857
+ }),
9858
+ dataset: object({
9859
+ versionId: string().min(1),
9860
+ annotationHash: string(),
9861
+ /** sha256 of the uploaded archive; the trainer refuses a mismatch (exit 65). */
9862
+ sha256: string().regex(/^[0-9a-f]{64}$/)
9863
+ }),
9864
+ split: object({
9865
+ holdoutCameras: array(number().int()),
9866
+ /** When no camera is held out: the last N days of every camera. */
9867
+ holdoutLastDays: number().int().nonnegative(),
9868
+ valFraction: number().min(0).max(.5),
9869
+ seed: number().int()
9870
+ }),
9871
+ hyper: object({
9872
+ epochs: number().int().positive(),
9873
+ batch: number().int().positive(),
9874
+ lr0: number().positive(),
9875
+ /** Layers frozen from the start of the backbone; 0 = none. */
9876
+ freeze: number().int().nonnegative(),
9877
+ patience: number().int().nonnegative(),
9878
+ seed: number().int()
9879
+ }),
9880
+ replay: object({
9881
+ source: literal("coco2017"),
9882
+ count: number().int().nonnegative()
9883
+ }),
9884
+ exports: array(TrainingExportSchema).min(1),
9885
+ calibration: object({ maxFrames: number().int().positive() }),
9886
+ floors: object({
9887
+ minSupport: number().int().positive(),
9888
+ clamp: number().min(0).max(1)
9889
+ }),
9890
+ int8Gate: object({ maxMap50Drop: number().min(0).max(1) }),
9891
+ catalog: object({
9892
+ id: string().regex(/^[a-z0-9][a-z0-9-]{2,80}$/),
9893
+ name: string().min(1).max(160)
9894
+ })
9895
+ });
9896
+ /** One regular file of the bundle. An mlpackage contributes one row per member. */
9897
+ var TrainingResultFileSchema = object({
9898
+ path: string().min(1),
9899
+ bytes: number().int().nonnegative(),
9900
+ sha256: string().regex(/^[0-9a-f]{64}$/),
9901
+ format: _enum([
9902
+ "onnx",
9903
+ "openvino",
9904
+ "coreml",
9905
+ "pt"
9906
+ ]),
9907
+ variant: _enum(["base", "int8"])
9908
+ });
9909
+ var DraftFormatSchema = object({
9910
+ /** RELATIVE path inside the bundle; the hub rewrites it to `camstack-local://`. */
9911
+ url: string().min(1),
9912
+ sizeMB: number().nonnegative(),
9913
+ isDirectory: boolean().optional(),
9914
+ files: array(string()).optional(),
9915
+ runtimes: array(literal("python")).optional(),
9916
+ revision: string().min(1)
9917
+ });
9918
+ /** The trainer's half of a catalog entry; the hub completes it (labels, licence, group). */
9919
+ var TrainingCatalogDraftSchema = object({
9920
+ id: string().min(1),
9921
+ name: string().min(1),
9922
+ description: string(),
9923
+ labels: array(unknown()),
9924
+ inputSize: object({
9925
+ width: number().int().positive(),
9926
+ height: number().int().positive()
9927
+ }),
9928
+ preprocessMode: literal("letterbox"),
9929
+ defaultClassFloors: FloorsSchema,
9930
+ formats: object({
9931
+ onnx: DraftFormatSchema.optional(),
9932
+ openvino: DraftFormatSchema.optional(),
9933
+ coreml: DraftFormatSchema.optional()
9934
+ })
9935
+ });
9936
+ /** One class of one model's holdout report (the eval harness's summary). */
9937
+ var TrainingEvalClassSchema = object({
9938
+ positives: number().int(),
9939
+ floor: number().nullable(),
9940
+ tp: number().int(),
9941
+ fp: number().int(),
9942
+ fn: number().int(),
9943
+ precision: number().nullable(),
9944
+ recall: number().nullable(),
9945
+ ap50: number().nullable(),
9946
+ recallByCamera: record(string(), number().nullable()).optional(),
9947
+ recallByLight: record(string(), number().nullable()).optional(),
9948
+ fpByCamera: record(string(), number()).optional(),
9949
+ smallFound: number().int().optional(),
9950
+ smallTotal: number().int().optional()
9951
+ }).passthrough();
9952
+ var TrainingEvalReportSchema = object({
9953
+ frames: number().int(),
9954
+ classes: record(string(), TrainingEvalClassSchema)
9955
+ }).passthrough();
9956
+ /**
9957
+ * A succeeded result carries every block; a failed one carries the error and
9958
+ * whatever was known by then — the other blocks are ABSENT, not null. The
9959
+ * orchestrator checks the blocks it needs before it trusts a success.
9960
+ */
9961
+ var TrainingResultSchema = object({
9962
+ schema: literal(TRAINING_RESULT_SCHEMA),
9963
+ jobKey: string(),
9964
+ status: _enum(["succeeded", "failed"]),
9965
+ error: object({
9966
+ stage: string(),
9967
+ code: string(),
9968
+ message: string()
9969
+ }).nullable(),
9970
+ trainer: object({
9971
+ version: string(),
9972
+ revision: string()
9973
+ }),
9974
+ toolchain: record(string(), string().nullable()),
9975
+ dataset: object({
9976
+ versionId: string(),
9977
+ annotationHash: string(),
9978
+ sha256: string(),
9979
+ frames: object({
9980
+ train: number().int(),
9981
+ val: number().int(),
9982
+ holdout: number().int()
9983
+ }),
9984
+ holdoutCameras: array(number().int()),
9985
+ replayImages: number().int()
9986
+ }).optional(),
9987
+ training: object({
9988
+ epochsRun: number().int(),
9989
+ bestEpoch: number().int(),
9990
+ bestMap50: number(),
9991
+ bestMap50_95: number(),
9992
+ seconds: number()
9993
+ }).optional(),
9994
+ evaluation: object({
9995
+ holdoutFrames: number().int(),
9996
+ baseline: TrainingEvalReportSchema.nullable(),
9997
+ candidate: TrainingEvalReportSchema.nullable(),
9998
+ candidateInt8: TrainingEvalReportSchema.nullable(),
9999
+ recallDeltaByCamera: record(string(), record(string(), number())).nullable(),
10000
+ map50: record(string(), number().nullable())
10001
+ }).optional(),
10002
+ floors: object({
10003
+ baseline: FloorsSchema,
10004
+ candidate: FloorsSchema,
10005
+ candidateInt8: FloorsSchema.nullable(),
10006
+ notes: array(string())
10007
+ }).optional(),
10008
+ int8: object({
10009
+ kept: boolean(),
10010
+ map50Fp16: number().nullable(),
10011
+ map50Int8: number().nullable(),
10012
+ reason: string().nullable()
10013
+ }).optional(),
10014
+ exportsDropped: array(object({
10015
+ export: string(),
10016
+ reason: string()
10017
+ })).optional(),
10018
+ files: array(TrainingResultFileSchema).optional(),
10019
+ bundle: object({
10020
+ path: literal("bundle.tar"),
10021
+ bytes: number().int().nonnegative(),
10022
+ sha256: string().regex(/^[0-9a-f]{64}$/)
10023
+ }).nullable().optional(),
10024
+ catalogDraft: object({
10025
+ base: TrainingCatalogDraftSchema,
10026
+ int8: TrainingCatalogDraftSchema.nullable()
10027
+ }).nullable().optional(),
10028
+ licence: string()
10029
+ });
10030
+ var TrainingStageSchema = _enum([
10031
+ "prepare",
10032
+ "replay",
10033
+ "train",
10034
+ "eval",
10035
+ "export",
10036
+ "package"
10037
+ ]);
10038
+ var EventBase = {
10039
+ v: literal(1),
10040
+ ts: number()
10041
+ };
10042
+ discriminatedUnion("type", [
10043
+ object({
10044
+ ...EventBase,
10045
+ type: literal("stage"),
10046
+ stage: TrainingStageSchema
10047
+ }),
10048
+ object({
10049
+ ...EventBase,
10050
+ type: literal("epoch"),
10051
+ epoch: number().int(),
10052
+ epochs: number().int(),
10053
+ boxLoss: number().nullable().optional(),
10054
+ map50: number().nullable().optional(),
10055
+ map50_95: number().nullable().optional(),
10056
+ etaSec: number().nullable().optional()
10057
+ }),
10058
+ object({
10059
+ ...EventBase,
10060
+ type: literal("metric"),
10061
+ name: string(),
10062
+ value: number()
10063
+ }),
10064
+ object({
10065
+ ...EventBase,
10066
+ type: literal("artifact"),
10067
+ path: string(),
10068
+ bytes: number(),
10069
+ sha256: string()
10070
+ }),
10071
+ object({
10072
+ ...EventBase,
10073
+ type: literal("done"),
10074
+ status: _enum(["succeeded", "failed"]),
10075
+ resultSha256: string().nullable().optional()
10076
+ }),
10077
+ object({
10078
+ ...EventBase,
10079
+ type: literal("error"),
10080
+ stage: string(),
10081
+ code: string(),
10082
+ message: string()
10083
+ })
10084
+ ]);
10085
+ /**
9391
10086
  * What the gate decided about a birth, from the CALLER's point of view.
9392
10087
  *
9393
10088
  * Deliberately not `ConfirmationVerdict`: `undecided` is not a birth decision
@@ -11715,7 +12410,9 @@ var AddonHttpRouteSchema = object({
11715
12410
  "authenticated",
11716
12411
  "admin"
11717
12412
  ]).optional(),
11718
- description: string().optional()
12413
+ description: string().optional(),
12414
+ /** The hub limits this route per client address before the handler runs. */
12415
+ rateLimit: _enum(["auth"]).optional()
11719
12416
  });
11720
12417
  /**
11721
12418
  * Cross-process route invocation envelope. The hub captures the
@@ -12420,6 +13117,37 @@ var BackupDestinationInfoSchema = object({
12420
13117
  lastError: string().optional()
12421
13118
  });
12422
13119
  /**
13120
+ * How an archive is encrypted (operator decision 2026-10-09): to a backup
13121
+ * PUBLIC key the hub holds, so only the operator's secret key opens it.
13122
+ * Absent on a plain `.tar.gz` — every archive made before encryption, or
13123
+ * with it off — which restores without a key.
13124
+ */
13125
+ var BackupArchiveEncryptionSchema = object({
13126
+ /** `x25519-hkdf-aes256gcm-stream-v1`. */
13127
+ scheme: string(),
13128
+ /** 16 hex — the backup key whose secret restores this archive. */
13129
+ keyId: string()
13130
+ });
13131
+ /**
13132
+ * The backup encryption setting. The hub keeps PUBLIC keys only: the active
13133
+ * one new archives are encrypted to, and the retired ones older archives
13134
+ * still need. The secret key is shown once, at `setupEncryption`.
13135
+ */
13136
+ var BackupEncryptionStatusSchema = object({
13137
+ enabled: boolean(),
13138
+ active: object({
13139
+ keyId: string(),
13140
+ publicKey: string(),
13141
+ createdAt: number()
13142
+ }).nullable(),
13143
+ /** Every key ever set up, newest first. */
13144
+ keys: array(object({
13145
+ keyId: string(),
13146
+ createdAt: number(),
13147
+ retiredAt: number().nullable()
13148
+ })).readonly()
13149
+ });
13150
+ /**
12423
13151
  * Per-archive entry returned by `backup.listArchives({ destinationId })`.
12424
13152
  * Same shape the destination drill-in renders. Sourced from the per-
12425
13153
  * location `manifests.json` (no per-archive tarball read required).
@@ -12431,7 +13159,9 @@ var BackupArchiveEntrySchema = object({
12431
13159
  sizeBytes: number(),
12432
13160
  label: string().optional(),
12433
13161
  /** Top-level locations included in the archive (db, addons, tls, …). */
12434
- locations: array(string()).readonly()
13162
+ locations: array(string()).readonly(),
13163
+ /** Present when the archive is encrypted; restore then needs that key. */
13164
+ encryption: BackupArchiveEncryptionSchema.optional()
12435
13165
  });
12436
13166
  var BackupEntrySchema = object({
12437
13167
  id: string(),
@@ -12440,7 +13170,8 @@ var BackupEntrySchema = object({
12440
13170
  label: string().optional(),
12441
13171
  createdAt: number(),
12442
13172
  sizeBytes: number(),
12443
- locations: array(string()).optional()
13173
+ locations: array(string()).optional(),
13174
+ encryption: BackupArchiveEncryptionSchema.optional()
12444
13175
  });
12445
13176
  var ArchiveEntrySchema = object({
12446
13177
  path: string(),
@@ -12629,10 +13360,40 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
12629
13360
  * this list and skips entries whose path doesn't start with
12630
13361
  * any of them.
12631
13362
  */
12632
- locations: array(string()).optional()
13363
+ locations: array(string()).optional(),
13364
+ /**
13365
+ * The backup SECRET key, for an encrypted archive (ignored for a plain
13366
+ * one). The archive is decrypted into the hub's private staging
13367
+ * before the restart; the key is not stored. A wrong or missing key
13368
+ * is refused before the archive is downloaded.
13369
+ */
13370
+ backupSecretKey: string().optional()
12633
13371
  }), _void(), {
12634
13372
  kind: "mutation",
12635
13373
  auth: "admin"
13374
+ }), method(_void(), BackupEncryptionStatusSchema, { auth: "admin" }), method(object({}), object({
13375
+ keyId: string(),
13376
+ secretKey: string(),
13377
+ publicKey: string(),
13378
+ createdAt: number(),
13379
+ replacedKeyId: string().nullable()
13380
+ }), {
13381
+ kind: "mutation",
13382
+ auth: "admin"
13383
+ }), method(object({}), object({ disabledKeyId: string().nullable() }), {
13384
+ kind: "mutation",
13385
+ auth: "admin"
13386
+ }), method(object({ backupSecretKey: string() }), object({
13387
+ valid: boolean(),
13388
+ keyId: string().nullable(),
13389
+ /** The hub knows this key (active or retired). */
13390
+ known: boolean(),
13391
+ /** It is the key new archives are encrypted to. */
13392
+ active: boolean(),
13393
+ error: string().optional()
13394
+ }), {
13395
+ kind: "mutation",
13396
+ auth: "admin"
12636
13397
  }), method(object({
12637
13398
  destinationId: string(),
12638
13399
  backupId: string()
@@ -14256,7 +15017,60 @@ var CustomModelDescriptorSchema = object({
14256
15017
  stepId: string(),
14257
15018
  entry: ModelCatalogEntrySchema
14258
15019
  });
14259
- method(_void(), array(CustomModelDescriptorSchema).readonly(), { auth: "admin" });
15020
+ /** One regular file of an imported bundle, as the producer measured it. */
15021
+ var ModelBundleFileSchema = object({
15022
+ /** Relative path inside the bundle tar; an mlpackage member is `<x>.mlpackage/<member>`. */
15023
+ path: string().min(1),
15024
+ bytes: number().int().nonnegative(),
15025
+ sha256: string().regex(/^[0-9a-f]{64}$/)
15026
+ });
15027
+ var ImportModelBundleInputSchema = object({
15028
+ /** The registry provider that keeps the model (Model Studio). */
15029
+ addonId: string(),
15030
+ stepId: string().min(1),
15031
+ /**
15032
+ * The catalog rows to register, complete, every format URL already
15033
+ * `camstack-local://<modelId>/<file>` naming a file of the bundle. One
15034
+ * bundle may carry several rows (a base and its INT8 variant).
15035
+ */
15036
+ entries: array(ModelCatalogEntrySchema).min(1).max(4),
15037
+ /** The bundle tar, offered by the producer over `ctx.peerBytes` (same host). */
15038
+ bundle: object({
15039
+ ticket: PeerBytesTicketSchema,
15040
+ bytes: number().int().positive(),
15041
+ sha256: string().regex(/^[0-9a-f]{64}$/)
15042
+ }),
15043
+ /** Every file the tar must hold; anything missing, extra or different is refused. */
15044
+ files: array(ModelBundleFileSchema).min(1).max(64),
15045
+ /** Convert missing engine formats and push the model to every agent afterwards. */
15046
+ distribute: boolean()
15047
+ });
15048
+ var ImportModelBundleResultSchema = discriminatedUnion("kind", [object({
15049
+ kind: literal("imported"),
15050
+ modelIds: array(string()).readonly(),
15051
+ distributed: array(object({
15052
+ modelId: string(),
15053
+ nodeId: string(),
15054
+ format: _enum(MODEL_FORMATS),
15055
+ ok: boolean(),
15056
+ error: string().optional()
15057
+ })).readonly()
15058
+ }), object({
15059
+ kind: literal("refused"),
15060
+ reason: _enum([
15061
+ "transfer-failed",
15062
+ "checksum-mismatch",
15063
+ "bundle-mismatch",
15064
+ "invalid-entry",
15065
+ "id-taken"
15066
+ ]),
15067
+ detail: string()
15068
+ })]);
15069
+ method(_void(), array(CustomModelDescriptorSchema).readonly(), { auth: "admin" }), method(ImportModelBundleInputSchema, ImportModelBundleResultSchema, {
15070
+ kind: "mutation",
15071
+ auth: "admin",
15072
+ timeoutMs: 30 * 6e4
15073
+ });
14260
15074
  /**
14261
15075
  * Query filter for settings-store collections.
14262
15076
  */
@@ -14735,6 +15549,35 @@ var KeychainKeySourceSchema = object({
14735
15549
  */
14736
15550
  sharesBackupWithDatabase: boolean()
14737
15551
  });
15552
+ /**
15553
+ * One keychain reset, as the Secrets page reports it until the operator
15554
+ * acknowledges it. Never a value: refs, counts and table names only.
15555
+ */
15556
+ var KeychainResetRecordSchema = object({
15557
+ id: string(),
15558
+ at: number(),
15559
+ /** `user:<id>` or {@link KEYCHAIN_RESET_TRIGGER_ACTOR}. */
15560
+ actor: string(),
15561
+ /** Why the keychain was locked when it was reset; `null` when it was not locked. */
15562
+ lockedReason: string().nullable(),
15563
+ /** Values moved into the archive table (ciphertext under the lost key). */
15564
+ archived: number().int(),
15565
+ /** `keychain:secrets-orphaned-<ts>`, or `null` when nothing was stored. */
15566
+ archivedCollection: string().nullable(),
15567
+ /**
15568
+ * `keychain:keys-orphaned-<ts>`: the wrapped data keys of the archive, kept
15569
+ * so the archive can still be opened if the lost master key turns up.
15570
+ */
15571
+ archivedKeysCollection: string().nullable(),
15572
+ /**
15573
+ * `system-settings` keys whose marker the reset cleared — the cluster
15574
+ * secret's: a marker left pointing at an archived value would refuse every
15575
+ * agent forever (D797).
15576
+ */
15577
+ clearedSettingsKeys: array(string()).readonly(),
15578
+ /** When an operator dismissed the notice on the Secrets page; `null` until then. */
15579
+ acknowledgedAt: number().nullable()
15580
+ });
14738
15581
  var KeychainStatusSchema = object({
14739
15582
  keychain: KeychainStateSchema,
14740
15583
  /** The source of the master key in use; `null` while locked. */
@@ -14752,7 +15595,9 @@ var KeychainStatusSchema = object({
14752
15595
  /** Wrapped copies of the data key under a previous master key, kept until purged. */
14753
15596
  retiredWraps: number().int(),
14754
15597
  recoveryKeyDownloadedAt: number().nullable(),
14755
- secretCount: number().int()
15598
+ secretCount: number().int(),
15599
+ /** The most recent reset, acknowledged or not; `null` when the keychain was never reset. */
15600
+ lastReset: KeychainResetRecordSchema.nullable()
14756
15601
  });
14757
15602
  /**
14758
15603
  * Whether a stored value is still pointed at by its owner's settings.
@@ -14774,13 +15619,14 @@ var KeychainAuditActionSchema = _enum([
14774
15619
  "download-recovery-key",
14775
15620
  "delete-orphans",
14776
15621
  "reset",
15622
+ "acknowledge-reset",
14777
15623
  "initialize"
14778
15624
  ]);
14779
15625
  var KeychainAuditEntrySchema = object({
14780
15626
  id: string(),
14781
15627
  at: number(),
14782
15628
  action: KeychainAuditActionSchema,
14783
- /** `addon:<id>` | `user:<id>` | `system`. */
15629
+ /** `addon:<id>` | `user:<id>` | `system` | `trigger-file`. */
14784
15630
  actor: string(),
14785
15631
  ref: string().nullable(),
14786
15632
  detail: string().nullable()
@@ -14846,6 +15692,13 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
14846
15692
  kind: "mutation",
14847
15693
  auth: "admin",
14848
15694
  caller: "required"
15695
+ }), method(object({}), object({
15696
+ reset: KeychainResetRecordSchema.nullable(),
15697
+ pendingReentry: array(KeychainSecretMetaSchema).readonly()
15698
+ }), { auth: "admin" }), method(object({}), object({ acknowledged: boolean() }), {
15699
+ kind: "mutation",
15700
+ auth: "admin",
15701
+ caller: "required"
14849
15702
  });
14850
15703
  method(_void(), object({
14851
15704
  id: string(),
@@ -17685,7 +18538,7 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
17685
18538
  * `login-method` — collection cap through which auth addons contribute
17686
18539
  * their pre-auth login surfaces to the login page. This is the SINGLE,
17687
18540
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
17688
- * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
18541
+ * every auth addon (OIDC, WebAuthn/passkey) registers a
17689
18542
  * `login-method` provider and the PUBLIC `auth.listLoginMethods`
17690
18543
  * procedure aggregates them for the unauthenticated login page.
17691
18544
  *
@@ -17693,8 +18546,7 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
17693
18546
  *
17694
18547
  * - `redirect` — a declarative button. The login page renders a generic
17695
18548
  * button that navigates to `startUrl` (an addon-owned HTTP route).
17696
- * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
17697
- * ZERO shell-side JS. A future SSO addon plugs in the same way — the
18549
+ * Covers OIDC (`/addon/auth-oidc/<id>/start`) with ZERO shell-side JS. A future SSO addon plugs in the same way — the
17698
18550
  * login page needs NO change.
17699
18551
  *
17700
18552
  * - `widget` — a Module-Federation widget the login page mounts (via
@@ -17713,8 +18565,8 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
17713
18565
  * decision.
17714
18566
  *
17715
18567
  * Every contribution carries a `stage`:
17716
- * - `primary` — shown on the first credentials screen (OIDC /
17717
- * magic-link buttons; a future usernameless passkey).
18568
+ * - `primary` — shown on the first credentials screen (OIDC
18569
+ * buttons; a usernameless passkey).
17718
18570
  * - `second-factor` — shown AFTER the password leg, gated on the
17719
18571
  * returned `factors` (passkey-as-2FA today).
17720
18572
  *
@@ -17741,7 +18593,7 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
17741
18593
  * The provider names an external identity a signed-in user can link to
17742
18594
  * their own account (D782): `auth.listLinkableProviders` /
17743
18595
  * `beginExternalLink` offer exactly the redirect methods that set this,
17744
- * whatever their id. Absent / false — a login button only (magic-link).
18596
+ * whatever their id. Absent / false — a login button only.
17745
18597
  */
17746
18598
  linkable: boolean().optional()
17747
18599
  }),
@@ -21558,8 +22410,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
21558
22410
  * `super_admin` — collapsed onto the single boolean because every
21559
22411
  * non-admin caller is governed by the same scope-check rules; the
21560
22412
  * distinction was only ever used to route role-specific procedures
21561
- * which we no longer have. Agents are admin sessions issued via
21562
- * `createServiceToken`; CLI / webhook integrations are `cst_*` tokens
22413
+ * which we no longer have. CLI / webhook integrations are `cst_*` tokens
21563
22414
  * with `isAdmin: false` + an explicit scope list.
21564
22415
  */
21565
22416
  /** Per-method access flavour. Materialised on every cap method via codegen. */
@@ -24254,6 +25105,17 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
24254
25105
  kind: "mutation",
24255
25106
  auth: "admin",
24256
25107
  timeoutMs: 60 * 6e4
25108
+ }), method(object({
25109
+ filter: RetrainExportFilterSchema,
25110
+ layout: RetrainExportLayoutSchema
25111
+ }), object({
25112
+ ticket: PeerBytesTicketSchema,
25113
+ frames: number().int(),
25114
+ annotations: number().int()
25115
+ }), {
25116
+ kind: "mutation",
25117
+ auth: "admin",
25118
+ timeoutMs: 12e4
24257
25119
  }), method(object({
24258
25120
  selection: RetrainDatasetSelectionSchema,
24259
25121
  completeOnly: boolean().optional()
@@ -27822,8 +28684,8 @@ version: string().optional() }), ServerUpdateActionResultSchema, {
27822
28684
  * `smtp-provider` — pluggable email delivery surface.
27823
28685
  *
27824
28686
  * Collection cap: a deployment may install multiple SMTP relays (e.g.
27825
- * one for transactional / one for notifications). Consumers (magic-link,
27826
- * notifier addons, …) walk the collection and pick the first
28687
+ * one for transactional / one for notifications). Consumers (notifier
28688
+ * addons, …) walk the collection and pick the first
27827
28689
  * ready provider OR target a specific addonId.
27828
28690
  *
27829
28691
  * The shape stays small on purpose — `sendEmail` is enough for the
@@ -28106,7 +28968,7 @@ targets: array(object({
28106
28968
  };
28107
28969
  /**
28108
28970
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
28109
- * providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
28971
+ * providers (OIDC, SAML, …) mint an HMAC-signed token
28110
28972
  * carrying the post-callback claims before redirecting back to
28111
28973
  * `/api/auth/sso/finish`.
28112
28974
  *
@@ -28169,7 +29031,14 @@ var SsoBridgeClaimsSchema = object({
28169
29031
  * Verified `iss` of the id_token behind an OIDC login bridge (D782). A
28170
29032
  * subject is unique only per issuer, so a link is bound to it.
28171
29033
  */
28172
- issuer: string().optional()
29034
+ issuer: string().optional(),
29035
+ /**
29036
+ * SHA-256 (hex) of the `camstack_sso_state` cookie the IdP `/start` set in
29037
+ * the browser that began this sign-in. `/api/auth/sso/finish` requires the
29038
+ * request to present the matching cookie — a login bridge is useless in
29039
+ * any other browser.
29040
+ */
29041
+ browserBinding: string().optional()
28173
29042
  });
28174
29043
  method(object({
28175
29044
  claims: SsoBridgeClaimsSchema,
@@ -38181,16 +39050,6 @@ var ApiKeySummarySchema = object({
38181
39050
  createdAt: number(),
38182
39051
  lastUsedAt: number().optional()
38183
39052
  });
38184
- var CreateApiKeyInputSchema = object({
38185
- label: string(),
38186
- isAdmin: boolean().default(false),
38187
- allowedProviders: union([array(string()), literal("*")]).optional(),
38188
- allowedDevices: record(string(), union([array(string()), literal("*")])).optional()
38189
- });
38190
- var CreateApiKeyResultSchema = object({
38191
- token: string(),
38192
- record: ApiKeySummarySchema
38193
- });
38194
39053
  var ScopedTokenSummarySchema = object({
38195
39054
  id: string(),
38196
39055
  userId: string(),
@@ -38289,17 +39148,10 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
38289
39148
  }), UserSummarySchema.extend({ passwordHash: string() }).nullable(), {
38290
39149
  kind: "mutation",
38291
39150
  access: "view"
38292
- }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(CreateApiKeyInputSchema, CreateApiKeyResultSchema, {
38293
- kind: "mutation",
38294
- auth: "admin",
38295
- access: "create"
38296
- }), method(object({ id: string() }), object({ success: literal(true) }), {
39151
+ }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: literal(true) }), {
38297
39152
  kind: "mutation",
38298
39153
  auth: "admin",
38299
39154
  access: "delete"
38300
- }), method(object({ token: string() }), ApiKeySummarySchema.nullable(), {
38301
- kind: "mutation",
38302
- access: "view"
38303
39155
  }), method(CreateScopedTokenInputSchema, CreateScopedTokenResultSchema, {
38304
39156
  kind: "mutation",
38305
39157
  auth: "admin",
@@ -38373,15 +39225,7 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
38373
39225
  }).nullable(), { access: "view" }), method(object({ userId: string() }), array(ExternalIdentitySchema), { auth: "admin" }), method(ExternalIdentityKeySchema, object({
38374
39226
  userId: string(),
38375
39227
  issuer: string()
38376
- }).nullable(), { auth: "admin" }), method(ExternalIdentityKeySchema.extend({
38377
- userId: string(),
38378
- issuer: string().min(1),
38379
- label: string().optional()
38380
- }), object({ success: literal(true) }), {
38381
- kind: "mutation",
38382
- auth: "admin",
38383
- access: "create"
38384
- }), method(ExternalIdentityKeySchema.extend({ userId: string() }), object({ success: boolean() }), {
39228
+ }).nullable(), { auth: "admin" }), method(ExternalIdentityKeySchema.extend({ userId: string() }), object({ success: boolean() }), {
38385
39229
  kind: "mutation",
38386
39230
  auth: "admin",
38387
39231
  access: "delete"
@@ -38776,6 +39620,451 @@ DeviceType.Camera, method(object({
38776
39620
  detection: array(ZoneRuleSchema).readonly(),
38777
39621
  package: array(ZoneRuleSchema).readonly()
38778
39622
  });
39623
+ /**
39624
+ * `training-provider` — ONE cloud that can run the CamStack trainer.
39625
+ *
39626
+ * A thin, mockable translation layer: it knows how to run a container with a
39627
+ * GPU, move a dataset in, read the job's stdout and hand artefacts back. It
39628
+ * knows nothing about datasets, splits, floors, cost policy or Model Studio —
39629
+ * those are the `training` orchestrator's (D805). Every rule written there
39630
+ * once is what makes a second provider (RunPod, a local NVIDIA node) a new
39631
+ * manifest entry and nothing else.
39632
+ *
39633
+ * Collection, routed by the `addonId` in every single-row input (the `llm` /
39634
+ * `notification-output` posture); `describe` concat-fans across providers.
39635
+ * Every method is `admin`: these spend money.
39636
+ *
39637
+ * Every cloud object a provider creates is tagged with the orchestrator's
39638
+ * `jobKey`, so a retried submit finds the job it already started instead of
39639
+ * starting a second GPU, and `listJobs` can find orphans.
39640
+ *
39641
+ * Bytes never ride a cap field: the dataset arrives as a `ctx.peerBytes`
39642
+ * ticket the producer (pipeline-analytics) minted, and artefacts leave as a
39643
+ * ticket the provider mints (D613).
39644
+ */
39645
+ var TrainingGpuOptionSchema = object({
39646
+ id: string(),
39647
+ label: string(),
39648
+ /** List price, USD per GPU-hour, before any region multiplier. */
39649
+ usdPerHour: number().nonnegative(),
39650
+ vramGb: number().nonnegative()
39651
+ });
39652
+ var TrainingRegionOptionSchema = object({
39653
+ /** `''` = any region (the provider's choice). */
39654
+ id: string(),
39655
+ label: string(),
39656
+ priceMultiplier: number().positive()
39657
+ });
39658
+ /**
39659
+ * The provider's per-account settings, as the orchestrator reads them. They
39660
+ * live in the PROVIDER addon's settings (operator decision 2026-10-08: they
39661
+ * depend on the user's account), never hard-coded in the orchestrator.
39662
+ */
39663
+ var TrainingProviderDefaultsSchema = object({
39664
+ gpuType: string(),
39665
+ region: string(),
39666
+ costCapUsd: number().nonnegative(),
39667
+ maxRuntimeSec: number().int().positive(),
39668
+ /** Hours a FAILED job's cloud dataset is kept for debugging; 0 = delete at once. */
39669
+ failedDatasetGraceHours: number().nonnegative()
39670
+ });
39671
+ var TrainingProviderDescriptorSchema = object({
39672
+ addonId: string(),
39673
+ /** `modal`, `runpod`, `fake`, … */
39674
+ kind: string(),
39675
+ label: string(),
39676
+ gpuTypes: array(TrainingGpuOptionSchema).readonly(),
39677
+ regions: array(TrainingRegionOptionSchema).readonly(),
39678
+ maxDatasetBytes: number().int().positive(),
39679
+ maxRuntimeSec: number().int().positive(),
39680
+ freeTierNote: string().optional(),
39681
+ /** Where the data goes, in one sentence the confirm dialog repeats. */
39682
+ dataResidencyNote: string(),
39683
+ /** Credentials are present — never the values. */
39684
+ configured: boolean(),
39685
+ defaults: TrainingProviderDefaultsSchema
39686
+ });
39687
+ var TrainingCredentialCheckSchema = object({
39688
+ ok: boolean(),
39689
+ /** Workspace / account name, when the provider says. */
39690
+ account: string().optional(),
39691
+ reason: _enum([
39692
+ "missing",
39693
+ "invalid",
39694
+ "forbidden",
39695
+ "network",
39696
+ "quota"
39697
+ ]).optional(),
39698
+ detail: string().optional()
39699
+ });
39700
+ var ProviderJobStateSchema = _enum([
39701
+ "pending",
39702
+ "starting",
39703
+ "running",
39704
+ "succeeded",
39705
+ "failed",
39706
+ "cancelled",
39707
+ "lost"
39708
+ ]);
39709
+ var ProviderJobStatusSchema = object({
39710
+ jobKey: string(),
39711
+ providerJobId: string().optional(),
39712
+ state: ProviderJobStateSchema,
39713
+ startedAt: number().optional(),
39714
+ endedAt: number().optional(),
39715
+ exitCode: number().int().optional(),
39716
+ failureReason: string().optional(),
39717
+ gpuType: string(),
39718
+ region: string().optional(),
39719
+ billedSeconds: number().nonnegative(),
39720
+ costUsd: number().nonnegative(),
39721
+ /** True when `costUsd` is runtime × list price, not a billing answer. */
39722
+ costIsEstimate: boolean(),
39723
+ lastFetchedAt: number()
39724
+ });
39725
+ /**
39726
+ * The trainer the job runs, by SOURCE revision: the provider turns it into an
39727
+ * image its own way (Modal builds the published Dockerfile at that revision;
39728
+ * a registry-pull provider maps it to a tag). Recorded on the job and echoed
39729
+ * in `result.json`.
39730
+ */
39731
+ var TrainerSourceRefSchema = object({
39732
+ /** Hugging Face repo id, e.g. `camstack/camstack-trainer`. */
39733
+ repo: string().min(1),
39734
+ /** A commit sha (never a branch) — the job must be reproducible. */
39735
+ revision: string().regex(/^[0-9a-f]{7,64}$/)
39736
+ });
39737
+ var JobRef = {
39738
+ addonId: string(),
39739
+ jobKey: string().uuid()
39740
+ };
39741
+ var TrainingLogLineSchema = object({
39742
+ ts: number(),
39743
+ text: string()
39744
+ });
39745
+ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({ addonId: string() }), TrainingCredentialCheckSchema, {
39746
+ kind: "mutation",
39747
+ auth: "admin",
39748
+ timeoutMs: 3e4
39749
+ }), method(object({
39750
+ addonId: string(),
39751
+ gpuType: string(),
39752
+ region: string(),
39753
+ datasetBytes: number().int().nonnegative(),
39754
+ gpuSecondsLow: number().nonnegative(),
39755
+ gpuSecondsHigh: number().nonnegative()
39756
+ }), object({
39757
+ usdLow: number().nonnegative(),
39758
+ usdHigh: number().nonnegative(),
39759
+ notes: array(string()).readonly()
39760
+ }), { auth: "admin" }), method(object({
39761
+ ...JobRef,
39762
+ ticket: PeerBytesTicketSchema
39763
+ }), object({
39764
+ bytes: number().int().nonnegative(),
39765
+ sha256: string()
39766
+ }), {
39767
+ kind: "mutation",
39768
+ auth: "admin",
39769
+ timeoutMs: 120 * 6e4
39770
+ }), method(object({
39771
+ ...JobRef,
39772
+ trainer: TrainerSourceRefSchema,
39773
+ jobSpecJson: string().min(2).max(64e3),
39774
+ gpuType: string(),
39775
+ region: string(),
39776
+ maxRuntimeSec: number().int().positive()
39777
+ }), ProviderJobStatusSchema, {
39778
+ kind: "mutation",
39779
+ auth: "admin",
39780
+ timeoutMs: 30 * 6e4
39781
+ }), method(object(JobRef), ProviderJobStatusSchema, {
39782
+ auth: "admin",
39783
+ timeoutMs: 6e4
39784
+ }), method(object({
39785
+ ...JobRef,
39786
+ cursor: string().optional(),
39787
+ max: number().int().min(1).max(500)
39788
+ }), object({
39789
+ lines: array(TrainingLogLineSchema).readonly(),
39790
+ cursor: string()
39791
+ }), {
39792
+ auth: "admin",
39793
+ timeoutMs: 6e4
39794
+ }), method(object(JobRef), object({ ok: literal(true) }), {
39795
+ kind: "mutation",
39796
+ auth: "admin",
39797
+ timeoutMs: 6e4
39798
+ }), method(object(JobRef), object({
39799
+ found: boolean(),
39800
+ text: string().optional()
39801
+ }), {
39802
+ kind: "mutation",
39803
+ auth: "admin",
39804
+ timeoutMs: 5 * 6e4
39805
+ }), method(object({
39806
+ ...JobRef,
39807
+ path: _enum(["bundle.tar"])
39808
+ }), object({ ticket: PeerBytesTicketSchema }), {
39809
+ kind: "mutation",
39810
+ auth: "admin",
39811
+ timeoutMs: 5 * 6e4
39812
+ }), method(object(JobRef), object({ deleted: array(string()).readonly() }), {
39813
+ kind: "mutation",
39814
+ auth: "admin",
39815
+ timeoutMs: 5 * 6e4
39816
+ }), method(object({ addonId: string() }), array(ProviderJobStatusSchema).readonly(), {
39817
+ auth: "admin",
39818
+ timeoutMs: 2 * 6e4
39819
+ });
39820
+ /**
39821
+ * `training` — the cloud-training orchestrator (D805). Hub-only singleton,
39822
+ * provided by `addon-training`.
39823
+ *
39824
+ * It owns what every provider must agree on, written once:
39825
+ *
39826
+ * - **Dataset versions.** Taken over from pipeline-analytics (D796): the rows
39827
+ * live here (D808); pipeline-analytics still CUTS a version (it owns the corpus and
39828
+ * the export plan) and reaches this store through the five row methods
39829
+ * below, so the Retrain page and the export did not change.
39830
+ * - **Jobs.** A persisted state machine, a reconcile loop against the
39831
+ * provider, a cost guard, a daily orphan sweep and the retention rules.
39832
+ * - **The return flow.** Verified artefacts → Model Studio
39833
+ * (`custom-model-registry.importModelBundle`) → canary on cameras →
39834
+ * rollback. Nothing is ever registered automatically.
39835
+ *
39836
+ * Every method is `admin`.
39837
+ */
39838
+ /** A version WITH its frame list — what the export and a job read. */
39839
+ var TrainingDatasetVersionRowSchema = RetrainDatasetVersionSchema.omit({ frameCount: true }).extend({ frameIds: array(string()).max(2e4).readonly() });
39840
+ /** A base model a detector job can fine-tune, and the baseline it is judged against. */
39841
+ var TrainingBaseModelSchema = object({
39842
+ modelId: string(),
39843
+ label: string(),
39844
+ weights: string(),
39845
+ imgsz: number().int(),
39846
+ baselineOnnxUrl: string(),
39847
+ baselineFloors: record(string(), number()),
39848
+ /** The base the fleet runs most — the UI's default. */
39849
+ recommended: boolean()
39850
+ });
39851
+ var TrainingPresetSchema = _enum([
39852
+ "quick",
39853
+ "standard",
39854
+ "thorough"
39855
+ ]);
39856
+ var TrainingJobRequestSchema = object({
39857
+ datasetVersionId: string().min(1),
39858
+ /** The `training-provider` addon that runs it. */
39859
+ addonId: string().min(1),
39860
+ gpuType: string().min(1),
39861
+ region: string(),
39862
+ baseModelId: string().min(1),
39863
+ preset: TrainingPresetSchema,
39864
+ /** Freeze the backbone's first N layers; `null` = the preset's choice. */
39865
+ freeze: number().int().nonnegative().nullable(),
39866
+ /** Cameras held out whole for evaluation; `null` = the orchestrator's default. */
39867
+ holdoutCameras: array(number().int()).nullable()
39868
+ });
39869
+ var TrainingSplitPlanSchema = object({
39870
+ holdoutCameras: array(number().int()).readonly(),
39871
+ /** Used when no camera is held out: the last N days of every camera. */
39872
+ holdoutLastDays: number().int().nonnegative(),
39873
+ /** Holdout subjects per class (camera holdout only; `null` = known only after the split). */
39874
+ holdoutSupport: record(string(), number().int()).nullable(),
39875
+ trainSubjects: number().int()
39876
+ });
39877
+ var TrainingRefusalReasonSchema = _enum([
39878
+ "unknown-dataset",
39879
+ "unknown-provider",
39880
+ "provider-not-configured",
39881
+ "unknown-base-model",
39882
+ "unknown-gpu",
39883
+ "dataset-too-large",
39884
+ "no-training-subjects",
39885
+ "job-already-running"
39886
+ ]);
39887
+ var TrainingJobPlanSchema = object({
39888
+ refusal: object({
39889
+ reason: TrainingRefusalReasonSchema,
39890
+ detail: string()
39891
+ }).nullable(),
39892
+ warnings: array(string()).readonly(),
39893
+ split: TrainingSplitPlanSchema.nullable(),
39894
+ estimate: object({
39895
+ datasetBytes: number().int(),
39896
+ uploadMinutesAt20Mbps: number(),
39897
+ gpuSecondsLow: number(),
39898
+ gpuSecondsHigh: number(),
39899
+ usdLow: number(),
39900
+ usdHigh: number(),
39901
+ notes: array(string()).readonly()
39902
+ }).nullable(),
39903
+ /** Pre-filled from the provider's settings; the operator may lower or raise them. */
39904
+ defaults: object({
39905
+ maxCostUsd: number(),
39906
+ maxRuntimeSec: number().int()
39907
+ }).nullable(),
39908
+ /** The sentence the confirm dialog repeats: where the frames go and when they are deleted. */
39909
+ dataNotice: string()
39910
+ });
39911
+ /**
39912
+ * Orchestrator-owned states, persisted. `uploading` covers exporting the
39913
+ * archive (it is streamed, never staged). Terminal: `failed`, `cancelled`,
39914
+ * `lost`, `expired`, `discarded`. `registered` and `canary` are where a
39915
+ * result lives after the operator acted on it.
39916
+ */
39917
+ var TrainingJobStateSchema = _enum([
39918
+ "uploading",
39919
+ "submitted",
39920
+ "starting",
39921
+ "running",
39922
+ "collecting",
39923
+ "ready",
39924
+ "registered",
39925
+ "canary",
39926
+ "failed",
39927
+ "cancelling",
39928
+ "cancelled",
39929
+ "lost",
39930
+ "expired",
39931
+ "discarded"
39932
+ ]);
39933
+ var TrainingFailureReasonSchema = _enum([
39934
+ "cost-cap",
39935
+ "runtime-cap",
39936
+ "upload-failed",
39937
+ "submit-failed",
39938
+ "trainer-failed",
39939
+ "result-invalid",
39940
+ "checksum-mismatch",
39941
+ "fetch-failed",
39942
+ "provider-error",
39943
+ "hub-restart-during-upload"
39944
+ ]);
39945
+ var TrainingProgressPointSchema = object({
39946
+ epoch: number().int(),
39947
+ map50: number().nullable(),
39948
+ map50_95: number().nullable()
39949
+ });
39950
+ var TrainingCanarySchema = object({
39951
+ deviceIds: array(number().int()).readonly(),
39952
+ since: number(),
39953
+ /** Every override the canary replaced, exactly, so rollback restores it. */
39954
+ previous: array(object({
39955
+ deviceId: number().int(),
39956
+ nodeId: string(),
39957
+ deviceKey: string(),
39958
+ patch: object({
39959
+ modelId: string().optional(),
39960
+ settings: record(string(), unknown()).readonly().optional()
39961
+ }).nullable()
39962
+ })).readonly()
39963
+ });
39964
+ var TrainingJobSchema = object({
39965
+ id: string(),
39966
+ idempotencyKey: string(),
39967
+ name: string(),
39968
+ createdAt: number(),
39969
+ updatedAt: number(),
39970
+ state: TrainingJobStateSchema,
39971
+ stage: TrainingStageSchema.nullable(),
39972
+ failure: object({
39973
+ reason: TrainingFailureReasonSchema,
39974
+ stage: string().nullable(),
39975
+ detail: string()
39976
+ }).nullable(),
39977
+ request: TrainingJobRequestSchema,
39978
+ maxCostUsd: number().nonnegative(),
39979
+ maxRuntimeSec: number().int().positive(),
39980
+ trainer: TrainerSourceRefSchema,
39981
+ datasetLayout: RetrainExportLayoutSchema,
39982
+ dataset: object({
39983
+ bytes: number().int(),
39984
+ sha256: string(),
39985
+ uploadedAt: number()
39986
+ }).nullable(),
39987
+ split: TrainingSplitPlanSchema,
39988
+ provider: object({
39989
+ state: ProviderJobStateSchema.nullable(),
39990
+ providerJobId: string().nullable(),
39991
+ startedAt: number().nullable(),
39992
+ endedAt: number().nullable(),
39993
+ billedSeconds: number(),
39994
+ costUsd: number(),
39995
+ costIsEstimate: boolean(),
39996
+ lastPolledAt: number().nullable()
39997
+ }),
39998
+ progress: object({
39999
+ epoch: number().int().nullable(),
40000
+ epochs: number().int().nullable(),
40001
+ etaSec: number().nullable(),
40002
+ series: array(TrainingProgressPointSchema).readonly()
40003
+ }),
40004
+ /** The last lines of the job's output (≤ 200), newest last. Never frame paths. */
40005
+ logTail: array(string()).readonly(),
40006
+ logCursor: string().nullable(),
40007
+ artifacts: object({
40008
+ bundleBytes: number().int(),
40009
+ bundleSha256: string(),
40010
+ fetchedAt: number(),
40011
+ /** When the staged artefacts are deleted from the hub. */
40012
+ expiresAt: number()
40013
+ }).nullable(),
40014
+ /** When the provider-side copy of the frames was deleted; null while it exists. */
40015
+ cloudDatasetDeletedAt: number().nullable(),
40016
+ /** A failed job's dataset is kept until then (the provider's grace), then deleted. */
40017
+ cloudCleanupDueAt: number().nullable(),
40018
+ /** Model ids registered from this job (base, and int8 when kept). */
40019
+ registeredModelIds: array(string()).readonly(),
40020
+ canary: TrainingCanarySchema.nullable()
40021
+ });
40022
+ var IdInput = object({ id: string().min(1) });
40023
+ method(TrainingDatasetVersionRowSchema, object({ stored: boolean() }), {
40024
+ kind: "mutation",
40025
+ auth: "admin"
40026
+ }), method(IdInput, TrainingDatasetVersionRowSchema.nullable(), { auth: "admin" }), method(object({}), array(RetrainDatasetVersionSchema).readonly(), { auth: "admin" }), method(object({ name: string() }), object({ id: string().nullable() }), { auth: "admin" }), method(IdInput, object({ removed: boolean() }), {
40027
+ kind: "mutation",
40028
+ auth: "admin"
40029
+ }), method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" }), method(TrainingJobRequestSchema, TrainingJobPlanSchema, {
40030
+ auth: "admin",
40031
+ timeoutMs: 6e4
40032
+ }), method(TrainingJobRequestSchema.extend({
40033
+ name: string().min(1).max(120),
40034
+ maxCostUsd: number().positive(),
40035
+ maxRuntimeSec: number().int().positive(),
40036
+ idempotencyKey: string().min(8).max(80)
40037
+ }), discriminatedUnion("kind", [object({
40038
+ kind: literal("started"),
40039
+ job: TrainingJobSchema
40040
+ }), object({
40041
+ kind: literal("refused"),
40042
+ reason: TrainingRefusalReasonSchema,
40043
+ detail: string()
40044
+ })]), {
40045
+ kind: "mutation",
40046
+ auth: "admin",
40047
+ timeoutMs: 12e4
40048
+ }), method(object({}), array(TrainingJobSchema).readonly(), { auth: "admin" }), method(IdInput, TrainingJobSchema.nullable(), { auth: "admin" }), method(IdInput, TrainingJobSchema, {
40049
+ kind: "mutation",
40050
+ auth: "admin",
40051
+ timeoutMs: 12e4
40052
+ }), method(IdInput, TrainingResultSchema.nullable(), { auth: "admin" }), method(IdInput.extend({ canaryDeviceIds: array(number().int()).readonly() }), TrainingJobSchema, {
40053
+ kind: "mutation",
40054
+ auth: "admin",
40055
+ timeoutMs: 30 * 6e4
40056
+ }), method(IdInput.extend({ deviceIds: array(number().int()).min(1).readonly() }), TrainingJobSchema, {
40057
+ kind: "mutation",
40058
+ auth: "admin",
40059
+ timeoutMs: 12e4
40060
+ }), method(IdInput, TrainingJobSchema, {
40061
+ kind: "mutation",
40062
+ auth: "admin",
40063
+ timeoutMs: 12e4
40064
+ }), method(IdInput, TrainingJobSchema, {
40065
+ kind: "mutation",
40066
+ auth: "admin"
40067
+ });
38779
40068
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
38780
40069
  Object.freeze({
38781
40070
  person: .5,
@@ -40218,6 +41507,12 @@ Object.freeze({
40218
41507
  addonId: null,
40219
41508
  access: "create"
40220
41509
  },
41510
+ "backup.checkBackupKey": {
41511
+ capName: "backup",
41512
+ capScope: "system",
41513
+ addonId: null,
41514
+ access: "create"
41515
+ },
40221
41516
  "backup.delete": {
40222
41517
  capName: "backup",
40223
41518
  capScope: "system",
@@ -40230,6 +41525,18 @@ Object.freeze({
40230
41525
  addonId: null,
40231
41526
  access: "delete"
40232
41527
  },
41528
+ "backup.disableEncryption": {
41529
+ capName: "backup",
41530
+ capScope: "system",
41531
+ addonId: null,
41532
+ access: "create"
41533
+ },
41534
+ "backup.getEncryption": {
41535
+ capName: "backup",
41536
+ capScope: "system",
41537
+ addonId: null,
41538
+ access: "view"
41539
+ },
40233
41540
  "backup.getEntries": {
40234
41541
  capName: "backup",
40235
41542
  capScope: "system",
@@ -40284,6 +41591,12 @@ Object.freeze({
40284
41591
  addonId: null,
40285
41592
  access: "create"
40286
41593
  },
41594
+ "backup.setupEncryption": {
41595
+ capName: "backup",
41596
+ capScope: "system",
41597
+ addonId: null,
41598
+ access: "create"
41599
+ },
40287
41600
  "backup.trigger": {
40288
41601
  capName: "backup",
40289
41602
  capScope: "system",
@@ -40836,6 +42149,12 @@ Object.freeze({
40836
42149
  addonId: null,
40837
42150
  access: "create"
40838
42151
  },
42152
+ "customModelRegistry.importModelBundle": {
42153
+ capName: "custom-model-registry",
42154
+ capScope: "system",
42155
+ addonId: null,
42156
+ access: "create"
42157
+ },
40839
42158
  "customModelRegistry.listModels": {
40840
42159
  capName: "custom-model-registry",
40841
42160
  capScope: "system",
@@ -42276,6 +43595,12 @@ Object.freeze({
42276
43595
  addonId: null,
42277
43596
  access: "view"
42278
43597
  },
43598
+ "keychainAdmin.acknowledgeReset": {
43599
+ capName: "keychain-admin",
43600
+ capScope: "system",
43601
+ addonId: null,
43602
+ access: "create"
43603
+ },
42279
43604
  "keychainAdmin.deleteOrphans": {
42280
43605
  capName: "keychain-admin",
42281
43606
  capScope: "system",
@@ -42312,6 +43637,12 @@ Object.freeze({
42312
43637
  addonId: null,
42313
43638
  access: "delete"
42314
43639
  },
43640
+ "keychainAdmin.resetReport": {
43641
+ capName: "keychain-admin",
43642
+ capScope: "system",
43643
+ addonId: null,
43644
+ access: "delete"
43645
+ },
42315
43646
  "keychainAdmin.reveal": {
42316
43647
  capName: "keychain-admin",
42317
43648
  capScope: "system",
@@ -44058,6 +45389,12 @@ Object.freeze({
44058
45389
  addonId: null,
44059
45390
  access: "view"
44060
45391
  },
45392
+ "pipelineAnalytics.offerRetrainArchive": {
45393
+ capName: "pipeline-analytics",
45394
+ capScope: "device",
45395
+ addonId: null,
45396
+ access: "create"
45397
+ },
44061
45398
  "pipelineAnalytics.ownersWithMedia": {
44062
45399
  capName: "pipeline-analytics",
44063
45400
  capScope: "device",
@@ -46596,6 +47933,174 @@ Object.freeze({
46596
47933
  addonId: null,
46597
47934
  access: "view"
46598
47935
  },
47936
+ "training.canaryResult": {
47937
+ capName: "training",
47938
+ capScope: "system",
47939
+ addonId: null,
47940
+ access: "create"
47941
+ },
47942
+ "training.cancelJob": {
47943
+ capName: "training",
47944
+ capScope: "system",
47945
+ addonId: null,
47946
+ access: "create"
47947
+ },
47948
+ "training.discardResult": {
47949
+ capName: "training",
47950
+ capScope: "system",
47951
+ addonId: null,
47952
+ access: "create"
47953
+ },
47954
+ "training.findDatasetVersionIdByName": {
47955
+ capName: "training",
47956
+ capScope: "system",
47957
+ addonId: null,
47958
+ access: "view"
47959
+ },
47960
+ "training.getDatasetVersion": {
47961
+ capName: "training",
47962
+ capScope: "system",
47963
+ addonId: null,
47964
+ access: "view"
47965
+ },
47966
+ "training.getJob": {
47967
+ capName: "training",
47968
+ capScope: "system",
47969
+ addonId: null,
47970
+ access: "view"
47971
+ },
47972
+ "training.getJobResult": {
47973
+ capName: "training",
47974
+ capScope: "system",
47975
+ addonId: null,
47976
+ access: "view"
47977
+ },
47978
+ "training.listBaseModels": {
47979
+ capName: "training",
47980
+ capScope: "system",
47981
+ addonId: null,
47982
+ access: "view"
47983
+ },
47984
+ "training.listDatasetVersions": {
47985
+ capName: "training",
47986
+ capScope: "system",
47987
+ addonId: null,
47988
+ access: "view"
47989
+ },
47990
+ "training.listJobs": {
47991
+ capName: "training",
47992
+ capScope: "system",
47993
+ addonId: null,
47994
+ access: "view"
47995
+ },
47996
+ "training.planJob": {
47997
+ capName: "training",
47998
+ capScope: "system",
47999
+ addonId: null,
48000
+ access: "view"
48001
+ },
48002
+ "training.putDatasetVersion": {
48003
+ capName: "training",
48004
+ capScope: "system",
48005
+ addonId: null,
48006
+ access: "create"
48007
+ },
48008
+ "training.registerResult": {
48009
+ capName: "training",
48010
+ capScope: "system",
48011
+ addonId: null,
48012
+ access: "create"
48013
+ },
48014
+ "training.removeDatasetVersion": {
48015
+ capName: "training",
48016
+ capScope: "system",
48017
+ addonId: null,
48018
+ access: "delete"
48019
+ },
48020
+ "training.rollbackCanary": {
48021
+ capName: "training",
48022
+ capScope: "system",
48023
+ addonId: null,
48024
+ access: "create"
48025
+ },
48026
+ "training.startJob": {
48027
+ capName: "training",
48028
+ capScope: "system",
48029
+ addonId: null,
48030
+ access: "create"
48031
+ },
48032
+ "trainingProvider.cancelJob": {
48033
+ capName: "training-provider",
48034
+ capScope: "system",
48035
+ addonId: null,
48036
+ access: "create"
48037
+ },
48038
+ "trainingProvider.cleanupJob": {
48039
+ capName: "training-provider",
48040
+ capScope: "system",
48041
+ addonId: null,
48042
+ access: "create"
48043
+ },
48044
+ "trainingProvider.describe": {
48045
+ capName: "training-provider",
48046
+ capScope: "system",
48047
+ addonId: null,
48048
+ access: "view"
48049
+ },
48050
+ "trainingProvider.estimateJob": {
48051
+ capName: "training-provider",
48052
+ capScope: "system",
48053
+ addonId: null,
48054
+ access: "view"
48055
+ },
48056
+ "trainingProvider.getJob": {
48057
+ capName: "training-provider",
48058
+ capScope: "system",
48059
+ addonId: null,
48060
+ access: "view"
48061
+ },
48062
+ "trainingProvider.listJobEvents": {
48063
+ capName: "training-provider",
48064
+ capScope: "system",
48065
+ addonId: null,
48066
+ access: "view"
48067
+ },
48068
+ "trainingProvider.listJobs": {
48069
+ capName: "training-provider",
48070
+ capScope: "system",
48071
+ addonId: null,
48072
+ access: "view"
48073
+ },
48074
+ "trainingProvider.offerArtifact": {
48075
+ capName: "training-provider",
48076
+ capScope: "system",
48077
+ addonId: null,
48078
+ access: "create"
48079
+ },
48080
+ "trainingProvider.readResult": {
48081
+ capName: "training-provider",
48082
+ capScope: "system",
48083
+ addonId: null,
48084
+ access: "create"
48085
+ },
48086
+ "trainingProvider.submitJob": {
48087
+ capName: "training-provider",
48088
+ capScope: "system",
48089
+ addonId: null,
48090
+ access: "create"
48091
+ },
48092
+ "trainingProvider.uploadDataset": {
48093
+ capName: "training-provider",
48094
+ capScope: "system",
48095
+ addonId: null,
48096
+ access: "create"
48097
+ },
48098
+ "trainingProvider.validateCredentials": {
48099
+ capName: "training-provider",
48100
+ capScope: "system",
48101
+ addonId: null,
48102
+ access: "create"
48103
+ },
46599
48104
  "turnProvider.getTurnServers": {
46600
48105
  capName: "turn-provider",
46601
48106
  capScope: "system",
@@ -46620,12 +48125,6 @@ Object.freeze({
46620
48125
  addonId: null,
46621
48126
  access: "create"
46622
48127
  },
46623
- "userManagement.createApiKey": {
46624
- capName: "user-management",
46625
- capScope: "system",
46626
- addonId: null,
46627
- access: "create"
46628
- },
46629
48128
  "userManagement.createScopedToken": {
46630
48129
  capName: "user-management",
46631
48130
  capScope: "system",
@@ -46662,12 +48161,6 @@ Object.freeze({
46662
48161
  addonId: null,
46663
48162
  access: "view"
46664
48163
  },
46665
- "userManagement.linkExternalIdentity": {
46666
- capName: "user-management",
46667
- capScope: "system",
46668
- addonId: null,
46669
- access: "create"
46670
- },
46671
48164
  "userManagement.listApiKeys": {
46672
48165
  capName: "user-management",
46673
48166
  capScope: "system",
@@ -46770,12 +48263,6 @@ Object.freeze({
46770
48263
  addonId: null,
46771
48264
  access: "create"
46772
48265
  },
46773
- "userManagement.validateApiKey": {
46774
- capName: "user-management",
46775
- capScope: "system",
46776
- addonId: null,
46777
- access: "view"
46778
- },
46779
48266
  "userManagement.validateCredentials": {
46780
48267
  capName: "user-management",
46781
48268
  capScope: "system",
@@ -50955,6 +52442,11 @@ Object.freeze({
50955
52442
  form: "single",
50956
52443
  optional: false
50957
52444
  }],
52445
+ "training.canaryResult": [{
52446
+ name: "deviceIds",
52447
+ form: "array",
52448
+ optional: false
52449
+ }],
50958
52450
  "update.getStatus": [{
50959
52451
  name: "deviceId",
50960
52452
  form: "single",
@@ -61564,6 +63056,9 @@ async function discoverOnvifCameras(timeout = 5e3) {
61564
63056
  //#endregion
61565
63057
  //#region src/addon.ts
61566
63058
  var ADDON_ID = "provider-onvif";
63059
+ /** Stored flat, under its dotted schema key — the store is not nested. */
63060
+ var DISCOVERY_TIMEOUT_KEY = "discovery.timeout";
63061
+ var DEFAULT_DISCOVERY_TIMEOUT_MS = 5e3;
61567
63062
  var OnvifProviderAddon = class extends BaseDeviceProvider {
61568
63063
  addonId = ADDON_ID;
61569
63064
  providerName = "ONVIF";
@@ -61580,7 +63075,7 @@ var OnvifProviderAddon = class extends BaseDeviceProvider {
61580
63075
  }
61581
63076
  async discoverDevices() {
61582
63077
  const config = await this._getAddonConfig();
61583
- const timeout = config.discovery?.timeout ?? 5e3;
63078
+ const timeout = config.discovery?.timeout ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
61584
63079
  this.ctx.logger.info("Starting ONVIF discovery", { meta: { timeoutMs: timeout } });
61585
63080
  const cameras = await discoverOnvifCameras(timeout);
61586
63081
  this.lastDiscovered = cameras;
@@ -61702,22 +63197,17 @@ var OnvifProviderAddon = class extends BaseDeviceProvider {
61702
63197
  return { sections: [{
61703
63198
  id: "discovery",
61704
63199
  title: "ONVIF Discovery",
61705
- description: "Auto-discover ONVIF cameras on the local network",
63200
+ description: "Find ONVIF cameras on the local network when you add a device",
61706
63201
  columns: 2,
61707
63202
  fields: [{
61708
- type: "boolean",
61709
- key: "discovery.enabled",
61710
- label: "Enable Auto-Discovery",
61711
- default: true
61712
- }, {
61713
63203
  type: "number",
61714
- key: "discovery.timeout",
63204
+ key: DISCOVERY_TIMEOUT_KEY,
61715
63205
  label: "Discovery Timeout",
61716
63206
  unit: "ms",
61717
63207
  min: 1e3,
61718
63208
  max: 3e4,
61719
63209
  step: 1e3,
61720
- default: 5e3
63210
+ default: DEFAULT_DISCOVERY_TIMEOUT_MS
61721
63211
  }]
61722
63212
  }, {
61723
63213
  id: "credentials",
@@ -61739,10 +63229,11 @@ var OnvifProviderAddon = class extends BaseDeviceProvider {
61739
63229
  async _getAddonConfig() {
61740
63230
  const raw = await this.resolveGlobalStore();
61741
63231
  const { defaultUsername, defaultPassword } = this.config;
63232
+ const storedTimeout = raw[DISCOVERY_TIMEOUT_KEY];
61742
63233
  return {
61743
63234
  id: typeof raw["id"] === "string" ? raw["id"] : "onvif-default",
61744
63235
  name: typeof raw["name"] === "string" ? raw["name"] : "ONVIF Cameras",
61745
- discovery: raw["discovery"],
63236
+ discovery: { timeout: typeof storedTimeout === "number" && Number.isFinite(storedTimeout) && storedTimeout > 0 ? storedTimeout : DEFAULT_DISCOVERY_TIMEOUT_MS },
61746
63237
  cameras: raw["cameras"],
61747
63238
  defaultUsername: defaultUsername !== "" ? defaultUsername : void 0,
61748
63239
  defaultPassword: defaultPassword !== "" ? defaultPassword : void 0