@camstack/addon-provider-tuya 0.2.122 → 0.2.123

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 +1771 -131
  2. package/dist/addon.mjs +1771 -131
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -6197,7 +6197,7 @@ var ZodIssueCode = {
6197
6197
  var ZodFirstPartyTypeKind;
6198
6198
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6199
6199
  //#endregion
6200
- //#region ../types/dist/sleep-Cr5YOk3m.mjs
6200
+ //#region ../types/dist/sleep-r3RWkGVx.mjs
6201
6201
  /**
6202
6202
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6203
6203
  * window to float samples (D455).
@@ -6314,7 +6314,7 @@ function isKeychainStoredField(field) {
6314
6314
  var SECRET_CLEAR = { $secretClear: true };
6315
6315
  /** The `'***'` token D634 redaction writes for non-admins. */
6316
6316
  var LEGACY_DEVICE_REDACTION = "***";
6317
- function isRecord$1(value) {
6317
+ function isRecord$2(value) {
6318
6318
  return typeof value === "object" && value !== null && !Array.isArray(value);
6319
6319
  }
6320
6320
  function makeSecretMarker(ref) {
@@ -6325,12 +6325,12 @@ function makeSecretMarker(ref) {
6325
6325
  }
6326
6326
  /** A stored marker: an object whose `$secret` is a non-empty string. */
6327
6327
  function isSecretMarker(value) {
6328
- if (!isRecord$1(value)) return false;
6328
+ if (!isRecord$2(value)) return false;
6329
6329
  const ref = value["$secret"];
6330
6330
  return typeof ref === "string" && ref.length > 0;
6331
6331
  }
6332
6332
  function isSecretClear(value) {
6333
- return isRecord$1(value) && value["$secretClear"] === true;
6333
+ return isRecord$2(value) && value["$secretClear"] === true;
6334
6334
  }
6335
6335
  /**
6336
6336
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -6846,6 +6846,13 @@ function globalSecretScope() {
6846
6846
  function encodeSegment(value) {
6847
6847
  return encodeURIComponent(value);
6848
6848
  }
6849
+ function decodeSegment(value) {
6850
+ try {
6851
+ return decodeURIComponent(value);
6852
+ } catch {
6853
+ return null;
6854
+ }
6855
+ }
6849
6856
  /**
6850
6857
  * The field path of a value inside an array item, addressed by the item's id:
6851
6858
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6854,6 +6861,75 @@ function encodeSegment(value) {
6854
6861
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6855
6862
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6856
6863
  }
6864
+ function parseScope(segments) {
6865
+ switch (segments[0]) {
6866
+ case "global": return {
6867
+ scope: GLOBAL_SCOPE,
6868
+ used: 1
6869
+ };
6870
+ case "node": {
6871
+ const nodeId = segments[1] !== void 0 ? decodeSegment(segments[1]) : null;
6872
+ return nodeId === null || nodeId.length === 0 ? null : {
6873
+ scope: {
6874
+ kind: "node",
6875
+ nodeId
6876
+ },
6877
+ used: 2
6878
+ };
6879
+ }
6880
+ case "device": {
6881
+ const raw = segments[1];
6882
+ if (raw === void 0 || !/^\d+$/.test(raw)) return null;
6883
+ return {
6884
+ scope: {
6885
+ kind: "device",
6886
+ deviceId: Number(raw)
6887
+ },
6888
+ used: 2
6889
+ };
6890
+ }
6891
+ case "integration": {
6892
+ const id = segments[1] !== void 0 ? decodeSegment(segments[1]) : null;
6893
+ return id === null || id.length === 0 ? null : {
6894
+ scope: {
6895
+ kind: "integration",
6896
+ integrationId: id
6897
+ },
6898
+ used: 2
6899
+ };
6900
+ }
6901
+ case "row": {
6902
+ const collection = segments[1] !== void 0 ? decodeSegment(segments[1]) : null;
6903
+ const rowId = segments[2] !== void 0 ? decodeSegment(segments[2]) : null;
6904
+ if (collection === null || rowId === null || collection.length === 0 || rowId.length === 0) return null;
6905
+ return {
6906
+ scope: {
6907
+ kind: "row",
6908
+ collection,
6909
+ rowId
6910
+ },
6911
+ used: 3
6912
+ };
6913
+ }
6914
+ default: return null;
6915
+ }
6916
+ }
6917
+ /** Take a ref apart; `null` when it is not a well-formed ref. */
6918
+ function parseSecretRef(ref) {
6919
+ if (!ref.startsWith("secret://")) return null;
6920
+ const segments = ref.slice(9).split("/");
6921
+ const owner = segments[0] !== void 0 ? decodeSegment(segments[0]) : null;
6922
+ if (owner === null || owner.length === 0) return null;
6923
+ const parsed = parseScope(segments.slice(1));
6924
+ if (parsed === null) return null;
6925
+ const fieldPath = segments.slice(1 + parsed.used).join("/");
6926
+ if (fieldPath.length === 0) return null;
6927
+ return {
6928
+ owner,
6929
+ scope: parsed.scope,
6930
+ fieldPath
6931
+ };
6932
+ }
6857
6933
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
6858
6934
  var KeychainWriteRefusedError = class extends Error {
6859
6935
  fieldPath;
@@ -6864,7 +6940,7 @@ var KeychainWriteRefusedError = class extends Error {
6864
6940
  }
6865
6941
  };
6866
6942
  var KEEP = { kind: "keep" };
6867
- function isRecord(value) {
6943
+ function isRecord$1(value) {
6868
6944
  return typeof value === "object" && value !== null && !Array.isArray(value);
6869
6945
  }
6870
6946
  function defaultMintId() {
@@ -6877,7 +6953,7 @@ function collectSecretMarkerRefs(value, out = []) {
6877
6953
  return out;
6878
6954
  }
6879
6955
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6880
- else if (isRecord(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6956
+ else if (isRecord$1(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6881
6957
  return out;
6882
6958
  }
6883
6959
  async function storedValueEquals(secrets, marker, candidate) {
@@ -6932,7 +7008,7 @@ function rowId(row, idKey) {
6932
7008
  return null;
6933
7009
  }
6934
7010
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
6935
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord) : [];
7011
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$1) : [];
6936
7012
  if (!Array.isArray(incoming)) {
6937
7013
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6938
7014
  return {
@@ -6948,7 +7024,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6948
7024
  const kept = /* @__PURE__ */ new Set();
6949
7025
  const rows = [];
6950
7026
  for (const row of incoming) {
6951
- if (!isRecord(row)) {
7027
+ if (!isRecord$1(row)) {
6952
7028
  rows.push(row);
6953
7029
  continue;
6954
7030
  }
@@ -6973,7 +7049,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6973
7049
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
6974
7050
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
6975
7051
  case "object": {
6976
- if (!isRecord(incoming)) {
7052
+ if (!isRecord$1(incoming)) {
6977
7053
  if (incoming === void 0) return KEEP;
6978
7054
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
6979
7055
  return {
@@ -6981,7 +7057,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6981
7057
  value: incoming
6982
7058
  };
6983
7059
  }
6984
- const storedObj = isRecord(stored) ? stored : {};
7060
+ const storedObj = isRecord$1(stored) ? stored : {};
6985
7061
  return {
6986
7062
  kind: "value",
6987
7063
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -7040,12 +7116,19 @@ async function transformSecretWrite(input) {
7040
7116
  }
7041
7117
  };
7042
7118
  }
7119
+ /** Is `ref` one of `owner`'s refs? */
7120
+ function isSecretRefOwnedBy(ref, owner) {
7121
+ return parseSecretRef(ref)?.owner === owner;
7122
+ }
7043
7123
  /**
7044
7124
  * Replace every marker in `value` with its keychain value. Returns the SAME
7045
7125
  * reference when there is no marker (no allocation, no keychain call).
7046
7126
  */
7047
- async function resolveSecretMarkers(value, secrets) {
7048
- const refs = collectSecretMarkerRefs(value);
7127
+ async function resolveSecretMarkers(value, secrets, options = {}) {
7128
+ const keepMarker = options.unresolved === "keep-marker";
7129
+ const owner = options.ownedBy;
7130
+ const inScope = (ref) => owner === void 0 || isSecretRefOwnedBy(ref, owner);
7131
+ const refs = collectSecretMarkerRefs(value).filter(inScope);
7049
7132
  if (refs.length === 0) return {
7050
7133
  value,
7051
7134
  unresolved: []
@@ -7069,10 +7152,18 @@ async function resolveSecretMarkers(value, secrets) {
7069
7152
  }
7070
7153
  const substitute = (v) => {
7071
7154
  if (isSecretMarker(v)) {
7155
+ if (!inScope(v.$secret)) return {
7156
+ keep: true,
7157
+ value: v
7158
+ };
7072
7159
  const answer = answers[v.$secret];
7073
- return answer?.kind === "value" ? {
7160
+ if (answer?.kind === "value") return {
7074
7161
  keep: true,
7075
7162
  value: answer.value
7163
+ };
7164
+ return keepMarker ? {
7165
+ keep: true,
7166
+ value: v
7076
7167
  } : {
7077
7168
  keep: false,
7078
7169
  value: void 0
@@ -7089,7 +7180,7 @@ async function resolveSecretMarkers(value, secrets) {
7089
7180
  value: items
7090
7181
  };
7091
7182
  }
7092
- if (isRecord(v)) {
7183
+ if (isRecord$1(v)) {
7093
7184
  const out = {};
7094
7185
  for (const [k, child] of Object.entries(v)) {
7095
7186
  const s = substitute(child);
@@ -7110,14 +7201,152 @@ async function resolveSecretMarkers(value, secrets) {
7110
7201
  unresolved
7111
7202
  };
7112
7203
  }
7113
- function touchesTree(tree, patch) {
7114
- if (tree.size === 0) return false;
7115
- for (const key of Object.keys(patch)) {
7116
- const at = key.indexOf("@");
7117
- if (tree.has(at > 0 ? key.slice(0, at) : key)) return true;
7204
+ /** An addon tried to replace a stored secret it could not read. Nothing was written. */
7205
+ var SecretOverwriteRefusedError = class extends Error {
7206
+ ref;
7207
+ reason;
7208
+ constructor(ref, reason) {
7209
+ 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.`);
7210
+ this.ref = ref;
7211
+ this.reason = reason;
7212
+ this.name = "SecretOverwriteRefusedError";
7213
+ }
7214
+ };
7215
+ function isRecord(value) {
7216
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7217
+ }
7218
+ function resolvedValue(pass, marker) {
7219
+ const answer = pass.answers[marker.$secret];
7220
+ return answer?.kind === "value" ? answer.value : null;
7221
+ }
7222
+ async function atMarker(pass, incoming, stored) {
7223
+ if (incoming === null) return stored;
7224
+ if (typeof incoming !== "string") return incoming;
7225
+ if (incoming === resolvedValue(pass, stored)) return stored;
7226
+ const intent = classifySecretWrite(incoming);
7227
+ if (intent.kind !== "set") return stored;
7228
+ const ref = stored.$secret;
7229
+ if (!isSecretRefOwnedBy(ref, pass.secrets.owner)) return incoming;
7230
+ const answer = pass.answers[ref];
7231
+ 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);
7232
+ try {
7233
+ await pass.secrets.put(ref, intent.value);
7234
+ } catch (err) {
7235
+ throw new KeychainWriteRefusedError(ref, err);
7236
+ }
7237
+ pass.changedRefs.push(ref);
7238
+ return stored;
7239
+ }
7240
+ function inArray(pass, incoming, stored) {
7241
+ return incoming.map((item, index) => {
7242
+ const twin = stored[index];
7243
+ if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
7244
+ if (isRecord(item) && isRecord(twin)) return inArrayRow(pass, item, twin);
7245
+ if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
7246
+ return item;
7247
+ });
7248
+ }
7249
+ function inArrayRow(pass, incoming, stored) {
7250
+ const out = { ...incoming };
7251
+ for (const [key, value] of Object.entries(incoming)) {
7252
+ const twin = stored[key];
7253
+ if (isSecretMarker(twin)) {
7254
+ if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
7255
+ } else if (isRecord(value) && isRecord(twin)) out[key] = inArrayRow(pass, value, twin);
7256
+ else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
7257
+ }
7258
+ return out;
7259
+ }
7260
+ async function walk(pass, incoming, stored) {
7261
+ if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
7262
+ if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
7263
+ if (!isRecord(incoming) || !isRecord(stored)) return incoming;
7264
+ const out = { ...incoming };
7265
+ for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
7266
+ return out;
7267
+ }
7268
+ /** Restore markers a write-back carries as values. See the file doc. */
7269
+ async function restoreUnchangedSecrets(input) {
7270
+ const refs = [];
7271
+ for (const key of Object.keys(input.patch)) collectSecretMarkerRefs(input.stored[key], refs);
7272
+ const owned = refs.filter((ref) => isSecretRefOwnedBy(ref, input.secrets.owner));
7273
+ if (owned.length === 0) return {
7274
+ patch: input.patch,
7275
+ changedRefs: []
7276
+ };
7277
+ const pass = {
7278
+ secrets: input.secrets,
7279
+ answers: await input.secrets.resolve([...new Set(owned)]),
7280
+ operatorWrite: input.operatorWrite,
7281
+ changedRefs: []
7282
+ };
7283
+ const out = {};
7284
+ for (const [key, value] of Object.entries(input.patch)) out[key] = await walk(pass, value, input.stored[key]);
7285
+ return {
7286
+ patch: out,
7287
+ changedRefs: pass.changedRefs
7288
+ };
7289
+ }
7290
+ /** Structural equality over JSON-shaped values (key order does not matter). */
7291
+ function isJsonEqual(a, b) {
7292
+ if (Object.is(a, b)) return true;
7293
+ if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
7294
+ if (isRecord(a) && isRecord(b)) {
7295
+ const keys = Object.keys(a);
7296
+ if (keys.length !== Object.keys(b).length) return false;
7297
+ return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
7118
7298
  }
7119
7299
  return false;
7120
7300
  }
7301
+ /** The patch without the keys whose value already equals the stored one. */
7302
+ function dropUnchangedKeys(patch, stored) {
7303
+ const out = {};
7304
+ for (const [key, value] of Object.entries(patch)) {
7305
+ if (Object.prototype.hasOwnProperty.call(stored, key) && isJsonEqual(value, stored[key])) continue;
7306
+ out[key] = value;
7307
+ }
7308
+ return out;
7309
+ }
7310
+ /** The top-level keys of a blob that hold at least one of `owner`'s markers. */
7311
+ function markerKeysOf(blob, owner) {
7312
+ const keys = /* @__PURE__ */ new Set();
7313
+ for (const [key, value] of Object.entries(blob)) if (collectSecretMarkerRefs(value).some((ref) => isSecretRefOwnedBy(ref, owner))) keys.add(key);
7314
+ return keys;
7315
+ }
7316
+ function nodeFromStored(value, owner) {
7317
+ if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
7318
+ if (!isRecord(value)) return null;
7319
+ const children = /* @__PURE__ */ new Map();
7320
+ for (const [key, child] of Object.entries(value)) {
7321
+ const node = nodeFromStored(child, owner);
7322
+ if (node !== null) children.set(key, node);
7323
+ }
7324
+ return children.size > 0 ? {
7325
+ kind: "object",
7326
+ children
7327
+ } : null;
7328
+ }
7329
+ /**
7330
+ * The declared secret tree, widened by every place the STORED blob holds a
7331
+ * marker under one of `keys` — so a marker the schema no longer declares (or
7332
+ * never declared: Wyze before b4906cec6) still gets the save path's
7333
+ * KEEP / SET / CLEAR semantics instead of being overwritten by whatever the
7334
+ * addon writes. Markers inside arrays are not added: a row has no id field to
7335
+ * match by unless the schema declares one. Only `owner`'s markers count: a
7336
+ * builtin that keeps another addon's marker (device-manager) never takes it
7337
+ * over.
7338
+ */
7339
+ function widenTreeWithStoredMarkers(declared, stored, keys, owner) {
7340
+ const added = /* @__PURE__ */ new Map();
7341
+ for (const key of keys) {
7342
+ const at = key.indexOf("@");
7343
+ const base = at > 0 ? key.slice(0, at) : key;
7344
+ if (declared.has(base) || added.has(base)) continue;
7345
+ const node = nodeFromStored(stored[key], owner);
7346
+ if (node !== null) added.set(base, node);
7347
+ }
7348
+ return added.size === 0 ? declared : new Map([...declared, ...added]);
7349
+ }
7121
7350
  /** The blob as stored now; a failed read REFUSES the write instead of guessing. */
7122
7351
  async function readStoredAddonBlob(raw) {
7123
7352
  const threeWay = raw.readAddonStoreResult;
@@ -7126,7 +7355,41 @@ async function readStoredAddonBlob(raw) {
7126
7355
  if (result.kind === "failed") throw new Error(`settings write refused: the stored blob could not be read (${result.error})`);
7127
7356
  return result.kind === "missing" ? {} : result.value;
7128
7357
  }
7129
- function createSecretAwareSettings(raw, opts) {
7358
+ function touchesTree(tree, patch) {
7359
+ if (tree.size === 0) return false;
7360
+ for (const key of Object.keys(patch)) {
7361
+ const at = key.indexOf("@");
7362
+ if (tree.has(at > 0 ? key.slice(0, at) : key)) return true;
7363
+ }
7364
+ return false;
7365
+ }
7366
+ /**
7367
+ * Which top-level keys of each store held markers when last seen. `undefined`
7368
+ * for a store not seen yet: its first write reads it, to be safe.
7369
+ */
7370
+ var MarkerKeyIndex = class {
7371
+ owner;
7372
+ byStore = /* @__PURE__ */ new Map();
7373
+ constructor(owner) {
7374
+ this.owner = owner;
7375
+ }
7376
+ seen(store, blob) {
7377
+ this.byStore.set(store, markerKeysOf(blob, this.owner));
7378
+ }
7379
+ wrote(store, patch) {
7380
+ const keys = this.byStore.get(store);
7381
+ if (keys === void 0) return;
7382
+ const now = markerKeysOf(patch, this.owner);
7383
+ for (const key of Object.keys(patch)) if (now.has(key)) keys.add(key);
7384
+ else keys.delete(key);
7385
+ }
7386
+ mayHoldMarkers(store, patch) {
7387
+ const keys = this.byStore.get(store);
7388
+ if (keys === void 0) return true;
7389
+ return Object.keys(patch).some((key) => keys.has(key));
7390
+ }
7391
+ };
7392
+ function createHandle(raw, opts) {
7130
7393
  const chains = /* @__PURE__ */ new Map();
7131
7394
  const serialize = (key, fn) => {
7132
7395
  const next = (chains.get(key) ?? Promise.resolve()).then(fn, fn);
@@ -7137,46 +7400,130 @@ function createSecretAwareSettings(raw, opts) {
7137
7400
  });
7138
7401
  return next;
7139
7402
  };
7140
- const write = async (chainKey, tree, scope, patch, readStored, rawWrite) => {
7141
- if (!touchesTree(tree, patch)) return rawWrite(patch);
7142
- return serialize(chainKey, async () => {
7403
+ const index = new MarkerKeyIndex(opts.secrets.owner);
7404
+ const resolveRead = async (store, blob) => {
7405
+ index.seen(store, blob);
7406
+ const { value, unresolved } = await resolveSecretMarkers(blob, opts.secrets, {
7407
+ unresolved: "keep-marker",
7408
+ ownedBy: opts.secrets.owner
7409
+ });
7410
+ if (unresolved.length > 0) opts.onUnresolved?.(unresolved);
7411
+ return value;
7412
+ };
7413
+ const write = async (store, declared, scope, patch, readStored, rawWrite) => {
7414
+ if (!touchesTree(declared, patch) && !index.mayHoldMarkers(store, patch)) {
7415
+ await rawWrite(patch);
7416
+ index.wrote(store, patch);
7417
+ return { changed: true };
7418
+ }
7419
+ return serialize(store, async () => {
7143
7420
  const stored = await readStored();
7421
+ index.seen(store, stored);
7422
+ const operatorWrite = opts.isOperatorWrite?.() === true;
7423
+ const restored = await restoreUnchangedSecrets({
7424
+ patch,
7425
+ stored,
7426
+ secrets: opts.secrets,
7427
+ operatorWrite
7428
+ });
7144
7429
  const result = await transformSecretWrite({
7145
- tree,
7430
+ tree: widenTreeWithStoredMarkers(declared, stored, Object.keys(restored.patch), opts.secrets.owner),
7146
7431
  secrets: opts.secrets,
7147
- patch,
7432
+ patch: restored.patch,
7148
7433
  stored,
7149
7434
  ...scope !== void 0 ? { scope } : {},
7150
- operatorWrite: opts.isOperatorWrite?.() === true
7435
+ operatorWrite
7151
7436
  });
7152
- if (Object.keys(result.patch).length > 0) await rawWrite(result.patch);
7437
+ const effective = dropUnchangedKeys(result.patch, stored);
7438
+ const wroteBlob = Object.keys(effective).length > 0;
7439
+ if (wroteBlob) {
7440
+ await rawWrite(effective);
7441
+ index.wrote(store, effective);
7442
+ }
7153
7443
  const { orphaned } = await result.afterWrite();
7154
7444
  if (orphaned.length > 0) opts.onOrphaned(orphaned);
7445
+ return { changed: wroteBlob || restored.changedRefs.length > 0 };
7155
7446
  });
7156
7447
  };
7157
- const wrapped = {
7158
- writeAddonStore: (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p)),
7159
- writeDeviceStore: (deviceId, patch) => write(`device:${deviceId}`, opts.deviceTree(), {
7160
- kind: "device",
7161
- deviceId
7162
- }, patch, () => raw.readDeviceStore(deviceId), (p) => raw.writeDeviceStore(deviceId, p))
7448
+ const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
7449
+ const readAddonStoreResult = raw.readAddonStoreResult;
7450
+ const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
7451
+ const readDeviceStoreBatch = raw.readDeviceStoreBatch;
7452
+ const overrides = {
7453
+ readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
7454
+ ...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
7455
+ const result = await readAddonStoreResult.call(raw);
7456
+ return result.kind === "value" ? {
7457
+ kind: "value",
7458
+ value: await resolveRead("addon", result.value)
7459
+ } : result;
7460
+ } } : {},
7461
+ ...readAddonStoreKeyResult !== void 0 ? { readAddonStoreKeyResult: async (key) => {
7462
+ const result = await readAddonStoreKeyResult.call(raw, key);
7463
+ if (result.kind !== "present") return result;
7464
+ const { value, unresolved } = await resolveSecretMarkers({ [key]: result.value }, opts.secrets, {
7465
+ unresolved: "keep-marker",
7466
+ ownedBy: opts.secrets.owner
7467
+ });
7468
+ if (unresolved.length > 0) opts.onUnresolved?.(unresolved);
7469
+ return {
7470
+ kind: "present",
7471
+ value: value[key]
7472
+ };
7473
+ } } : {},
7474
+ readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
7475
+ ...readDeviceStoreBatch !== void 0 ? { readDeviceStoreBatch: async (deviceIds) => {
7476
+ const rows = await readDeviceStoreBatch.call(raw, deviceIds);
7477
+ const out = /* @__PURE__ */ new Map();
7478
+ for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
7479
+ return out;
7480
+ } } : {},
7481
+ writeAddonStore: async (patch) => {
7482
+ await writeAddon(patch);
7483
+ },
7484
+ writeDeviceStore: async (deviceId, patch) => {
7485
+ await write(`device:${deviceId}`, opts.deviceTree(), {
7486
+ kind: "device",
7487
+ deviceId
7488
+ }, patch, () => raw.readDeviceStore(deviceId), (p) => raw.writeDeviceStore(deviceId, p));
7489
+ }
7490
+ };
7491
+ return {
7492
+ settings: new Proxy(raw, { get(target, prop, receiver) {
7493
+ if (typeof prop === "string" && Object.prototype.hasOwnProperty.call(overrides, prop)) return Reflect.get(overrides, prop);
7494
+ const value = Reflect.get(target, prop, receiver);
7495
+ return typeof value === "function" ? value.bind(target) : value;
7496
+ } }),
7497
+ raw: {
7498
+ readAddonStore: async () => {
7499
+ const blob = await raw.readAddonStore();
7500
+ index.seen("addon", blob);
7501
+ return blob;
7502
+ },
7503
+ readDeviceStore: async (deviceId) => {
7504
+ const blob = await raw.readDeviceStore(deviceId);
7505
+ index.seen(`device:${deviceId}`, blob);
7506
+ return blob;
7507
+ }
7508
+ },
7509
+ writeAddonStore: writeAddon
7163
7510
  };
7164
- return new Proxy(raw, { get(target, prop, receiver) {
7165
- if (prop === "writeAddonStore") return wrapped.writeAddonStore;
7166
- if (prop === "writeDeviceStore") return wrapped.writeDeviceStore;
7167
- const value = Reflect.get(target, prop, receiver);
7168
- return typeof value === "function" ? value.bind(target) : value;
7169
- } });
7170
7511
  }
7171
- /** The context the addon sees: identical, except that its settings writes are secret-aware. */
7172
- function withSecretAwareSettings(context, opts) {
7512
+ /** The context the addon sees, plus the private raw view and write outcome. */
7513
+ function createSecretAwareContext(context, opts) {
7173
7514
  const raw = context.settings;
7174
- if (raw === void 0) return context;
7175
- const settings = createSecretAwareSettings(raw, opts);
7176
- return new Proxy(context, { get(target, prop, receiver) {
7177
- if (prop === "settings") return settings;
7178
- return Reflect.get(target, prop, receiver);
7179
- } });
7515
+ if (raw === void 0) return {
7516
+ context,
7517
+ handle: null
7518
+ };
7519
+ const handle = createHandle(raw, opts);
7520
+ return {
7521
+ context: new Proxy(context, { get(target, prop, receiver) {
7522
+ if (prop === "settings") return handle.settings;
7523
+ return Reflect.get(target, prop, receiver);
7524
+ } }),
7525
+ handle
7526
+ };
7180
7527
  }
7181
7528
  var LEAF = { kind: "leaf" };
7182
7529
  var DEFAULT_ITEM_ID_KEY = "id";
@@ -7351,6 +7698,14 @@ var BaseAddon = class {
7351
7698
  _secretChangeUnsub = null;
7352
7699
  /** >0 while an OPERATOR's settings write is being applied. */
7353
7700
  _operatorWriteDepth = 0;
7701
+ /**
7702
+ * The keychain-aware settings handle (D798). Its `raw` view is the ONLY way
7703
+ * to the marker-bearing blobs and stays private to this class: the form
7704
+ * hydrate and config resolution need markers, addon code never does.
7705
+ */
7706
+ _secretSettings = null;
7707
+ /** Refs an addon-facing read could not resolve — each is logged once per process. */
7708
+ _loggedUnresolvedReads = /* @__PURE__ */ new Set();
7354
7709
  constructor(defaults) {
7355
7710
  this.defaults = defaults;
7356
7711
  this._config = { ...defaults };
@@ -7387,13 +7742,21 @@ var BaseAddon = class {
7387
7742
  }
7388
7743
  async initialize(context) {
7389
7744
  const secrets = context.secrets;
7390
- this._ctx = secrets !== void 0 ? withSecretAwareSettings(context, {
7391
- secrets,
7392
- addonTree: () => this.addonSecretTree(),
7393
- deviceTree: () => this.deviceSecretTree(),
7394
- onOrphaned: (refs) => context.logger.warn("keychain refs dropped by a settings write could not be deleted", { meta: { refs: [...refs] } }),
7395
- isOperatorWrite: () => this._operatorWriteDepth > 0
7396
- }) : context;
7745
+ if (secrets !== void 0) {
7746
+ const { context: wrapped, handle } = createSecretAwareContext(context, {
7747
+ secrets,
7748
+ addonTree: () => this.addonSecretTree(),
7749
+ deviceTree: () => this.deviceSecretTree(),
7750
+ onOrphaned: (refs) => context.logger.warn("keychain refs dropped by a settings write could not be deleted", { meta: { refs: [...refs] } }),
7751
+ isOperatorWrite: () => this._operatorWriteDepth > 0,
7752
+ onUnresolved: (unresolved) => this.logUnresolvedRead(unresolved)
7753
+ });
7754
+ this._ctx = wrapped;
7755
+ this._secretSettings = handle;
7756
+ } else {
7757
+ this._ctx = context;
7758
+ this._secretSettings = null;
7759
+ }
7397
7760
  await this.resolveConfig();
7398
7761
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7399
7762
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7510,11 +7873,8 @@ var BaseAddon = class {
7510
7873
  async getGlobalSettings(overlay, cap, nodeId) {
7511
7874
  const schema = this.globalSettingsSchema(cap);
7512
7875
  if (!schema) return { sections: [] };
7513
- const projected = await this.resolveGlobalStore(nodeId, cap);
7514
- return hydrateSchema(schema, overlay ? {
7515
- ...projected,
7516
- ...overlay
7517
- } : projected, { inlineSecretsAsViews: true });
7876
+ const projected = await this.readRawProjectedStore(nodeId, cap);
7877
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
7518
7878
  }
7519
7879
  /**
7520
7880
  * The integration-level view of this addon's settings: exactly the sections
@@ -7559,7 +7919,7 @@ var BaseAddon = class {
7559
7919
  });
7560
7920
  }
7561
7921
  if (sections.length === 0) return null;
7562
- const projected = await this.resolveGlobalStore(nodeId);
7922
+ const projected = await this.readRawProjectedStore(nodeId);
7563
7923
  return hydrateSchema({
7564
7924
  ...schema,
7565
7925
  sections
@@ -7576,12 +7936,50 @@ var BaseAddon = class {
7576
7936
  * the store for custom option logic (option narrowing, value snapping) to
7577
7937
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
7578
7938
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
7939
+ *
7940
+ * Keychain values come back RESOLVED (D798): a secret reads as its value,
7941
+ * never as a `{ $secret }` marker. One the keychain cannot serve right now
7942
+ * stays a marker — "stored, unavailable" (`isSecretMarker()` is true, and it
7943
+ * is listed in {@link unresolvedSecrets}) — never `''` and never absent, so
7944
+ * it cannot be mistaken for "nothing stored, mint a new one". Writing the
7945
+ * resolved blob back is a no-op for every unchanged secret.
7579
7946
  */
7580
7947
  async resolveGlobalStore(nodeId, cap) {
7581
7948
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
7949
+ return this.projectForNode(raw, nodeId, cap);
7950
+ }
7951
+ projectForNode(store, nodeId, cap) {
7582
7952
  const keys = this.perNodeKeys(cap);
7583
7953
  const node = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
7584
- return keys.size > 0 ? projectStore(raw, keys, node) : raw;
7954
+ return keys.size > 0 ? projectStore(store, keys, node) : store;
7955
+ }
7956
+ /** The PROJECTED store with its markers — the form hydrate only (D798). */
7957
+ async readRawProjectedStore(nodeId, cap) {
7958
+ const raw = await this.rawSettings()?.readAddonStore() ?? {};
7959
+ return this.projectForNode(raw, nodeId, cap);
7960
+ }
7961
+ /**
7962
+ * `{ ...stored, ...overlay }`, except that an overlay member equal to the
7963
+ * RESOLVED stored one keeps the stored (marker-bearing) member — so a form
7964
+ * hydrated from a resolved overlay still shows a secret as "set", never as
7965
+ * its value.
7966
+ */
7967
+ async overlayKeepingMarkers(stored, overlay) {
7968
+ const secrets = this._ctx?.secrets;
7969
+ const merged = {
7970
+ ...stored,
7971
+ ...overlay
7972
+ };
7973
+ if (secrets === void 0 || collectSecretMarkerRefs(stored).length === 0) return merged;
7974
+ const { value: resolved } = await resolveSecretMarkers(stored, secrets, {
7975
+ unresolved: "keep-marker",
7976
+ ownedBy: secrets.owner
7977
+ });
7978
+ for (const key of Object.keys(overlay)) {
7979
+ if (collectSecretMarkerRefs(stored[key]).length === 0) continue;
7980
+ if (isJsonEqual(overlay[key], resolved[key])) merged[key] = stored[key];
7981
+ }
7982
+ return merged;
7585
7983
  }
7586
7984
  /**
7587
7985
  * Whether ONE global key of this addon's store was written, read three-way
@@ -7646,8 +8044,12 @@ var BaseAddon = class {
7646
8044
  async clearSecrets(keys, nodeId) {
7647
8045
  const tree = this.addonSecretTree();
7648
8046
  const keychain = this._ctx?.secrets !== void 0;
8047
+ const stored = keychain ? await this.readAddonStoreWithRetry() : {};
7649
8048
  const patch = {};
7650
- for (const key of keys) patch[key] = keychain && tree.has(key) ? SECRET_CLEAR : "";
8049
+ for (const key of keys) {
8050
+ const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8051
+ patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8052
+ }
7651
8053
  await this.applyGlobalPatch(patch, nodeId);
7652
8054
  }
7653
8055
  async applyGlobalPatch(barePatch, nodeId) {
@@ -7655,7 +8057,11 @@ var BaseAddon = class {
7655
8057
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
7656
8058
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
7657
8059
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
7658
- await this._ctx?.settings?.writeAddonStore(scoped);
8060
+ const handle = this._secretSettings;
8061
+ if (handle !== null) {
8062
+ const { changed } = await handle.writeAddonStore(scoped);
8063
+ if (!changed) return;
8064
+ } else await this._ctx?.settings?.writeAddonStore(scoped);
7659
8065
  if (target !== localNode) return;
7660
8066
  await this.resolveConfig();
7661
8067
  await this.onConfigChanged();
@@ -7708,7 +8114,7 @@ var BaseAddon = class {
7708
8114
  async getDeviceSettings(deviceId) {
7709
8115
  const schema = this.deviceSettingsSchema();
7710
8116
  if (!schema) return { sections: [] };
7711
- return hydrateSchema(schema, await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, { inlineSecretsAsViews: true });
8117
+ return hydrateSchema(schema, await this.rawSettings()?.readDeviceStore(deviceId) ?? {}, { inlineSecretsAsViews: true });
7712
8118
  }
7713
8119
  async updateDeviceSettings(deviceId, patch) {
7714
8120
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
@@ -7880,7 +8286,7 @@ var BaseAddon = class {
7880
8286
  key,
7881
8287
  schema,
7882
8288
  fallback,
7883
- read: async () => this.resolveStoredSecrets(await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, false),
8289
+ read: async () => this.resolveStoredSecrets(await this.rawSettings()?.readDeviceStore(deviceId) ?? {}, false),
7884
8290
  write: async (patch) => {
7885
8291
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
7886
8292
  },
@@ -7942,6 +8348,23 @@ var BaseAddon = class {
7942
8348
  }
7943
8349
  return value;
7944
8350
  }
8351
+ /**
8352
+ * The marker-bearing settings reads (D798) — private on purpose. Without a
8353
+ * keychain the context's own view is already raw.
8354
+ */
8355
+ rawSettings() {
8356
+ return this._secretSettings?.raw ?? this._ctx?.settings;
8357
+ }
8358
+ /** An addon-facing read met markers it could not resolve: say so once per ref. */
8359
+ logUnresolvedRead(unresolved) {
8360
+ const fresh = unresolved.filter((u) => !this._loggedUnresolvedReads.has(u.ref));
8361
+ if (fresh.length === 0) return;
8362
+ for (const u of fresh) this._loggedUnresolvedReads.add(u.ref);
8363
+ 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) => ({
8364
+ ref: u.ref,
8365
+ reason: u.reason
8366
+ })) } });
8367
+ }
7945
8368
  /** Re-resolve the config (a keychain-only change, or a retry) and apply it. */
7946
8369
  scheduleSecretReresolve(delayMs) {
7947
8370
  const timerOf = delayMs === SECRET_CHANGE_DEBOUNCE_MS ? "_secretChangeTimer" : "_secretRetryTimer";
@@ -7968,7 +8391,7 @@ var BaseAddon = class {
7968
8391
  * `{}` (defaults) — the addon's first successful patch will rehydrate.
7969
8392
  */
7970
8393
  async readAddonStoreWithRetry() {
7971
- const settings = this._ctx?.settings;
8394
+ const settings = this.rawSettings();
7972
8395
  if (!settings) return {};
7973
8396
  const delaysMs = [
7974
8397
  150,
@@ -10271,6 +10694,278 @@ var CameraSwitchGroupSchema = object({
10271
10694
  fetchedAt: number()
10272
10695
  });
10273
10696
  /**
10697
+ * The trainer contract — what the hub hands the cloud trainer and what it
10698
+ * gets back (cloud training, D805+).
10699
+ *
10700
+ * The trainer (`hf/camstack-trainer`, published as the Hugging Face repo
10701
+ * `camstack/camstack-trainer`) is a separate AGPL program the hub never
10702
+ * imports: `job.json` goes in, files come out, and progress is `CAMSTACK_EVENT`
10703
+ * lines on stdout. These schemas are the hub's half of that boundary; the
10704
+ * trainer's pydantic models (and the JSON Schemas generated from them under
10705
+ * `hf/camstack-trainer/schemas/`) are the other half. A change here is a
10706
+ * change there, in the same piece of work.
10707
+ *
10708
+ * No `.default()` anywhere: defaults do not run on the addon cap path, so the
10709
+ * orchestrator fills every field of a job spec explicitly.
10710
+ */
10711
+ var TRAINING_JOB_SCHEMA = "camstack.training/v1";
10712
+ var TRAINING_RESULT_SCHEMA = "camstack.training-result/v1";
10713
+ /** What the trainer can export. Edge TPU is a later, separate image. */
10714
+ var TrainingExportSchema = _enum([
10715
+ "onnx",
10716
+ "openvino-fp16",
10717
+ "openvino-int8",
10718
+ "coreml-fp16"
10719
+ ]);
10720
+ var FloorsSchema = record(string(), number().min(0).max(1));
10721
+ object({
10722
+ schema: literal(TRAINING_JOB_SCHEMA),
10723
+ jobKey: string().uuid(),
10724
+ target: object({
10725
+ kind: literal("detector"),
10726
+ step: literal("object-detection")
10727
+ }),
10728
+ base: object({
10729
+ modelId: string().min(1),
10730
+ /** Ultralytics weights id, e.g. `yolov9m.pt`. */
10731
+ weights: string().min(1),
10732
+ imgsz: number().int().positive()
10733
+ }),
10734
+ baseline: object({
10735
+ modelId: string().min(1),
10736
+ /** The baseline ONNX the holdout comparison runs; public mirror URL. */
10737
+ onnxUrl: string().url(),
10738
+ imgsz: number().int().positive(),
10739
+ floors: FloorsSchema
10740
+ }),
10741
+ dataset: object({
10742
+ versionId: string().min(1),
10743
+ annotationHash: string(),
10744
+ /** sha256 of the uploaded archive; the trainer refuses a mismatch (exit 65). */
10745
+ sha256: string().regex(/^[0-9a-f]{64}$/)
10746
+ }),
10747
+ split: object({
10748
+ holdoutCameras: array(number().int()),
10749
+ /** When no camera is held out: the last N days of every camera. */
10750
+ holdoutLastDays: number().int().nonnegative(),
10751
+ valFraction: number().min(0).max(.5),
10752
+ seed: number().int()
10753
+ }),
10754
+ hyper: object({
10755
+ epochs: number().int().positive(),
10756
+ batch: number().int().positive(),
10757
+ lr0: number().positive(),
10758
+ /** Layers frozen from the start of the backbone; 0 = none. */
10759
+ freeze: number().int().nonnegative(),
10760
+ patience: number().int().nonnegative(),
10761
+ seed: number().int()
10762
+ }),
10763
+ replay: object({
10764
+ source: literal("coco2017"),
10765
+ count: number().int().nonnegative()
10766
+ }),
10767
+ exports: array(TrainingExportSchema).min(1),
10768
+ calibration: object({ maxFrames: number().int().positive() }),
10769
+ floors: object({
10770
+ minSupport: number().int().positive(),
10771
+ clamp: number().min(0).max(1)
10772
+ }),
10773
+ int8Gate: object({ maxMap50Drop: number().min(0).max(1) }),
10774
+ catalog: object({
10775
+ id: string().regex(/^[a-z0-9][a-z0-9-]{2,80}$/),
10776
+ name: string().min(1).max(160)
10777
+ })
10778
+ });
10779
+ /** One regular file of the bundle. An mlpackage contributes one row per member. */
10780
+ var TrainingResultFileSchema = object({
10781
+ path: string().min(1),
10782
+ bytes: number().int().nonnegative(),
10783
+ sha256: string().regex(/^[0-9a-f]{64}$/),
10784
+ format: _enum([
10785
+ "onnx",
10786
+ "openvino",
10787
+ "coreml",
10788
+ "pt"
10789
+ ]),
10790
+ variant: _enum(["base", "int8"])
10791
+ });
10792
+ var DraftFormatSchema = object({
10793
+ /** RELATIVE path inside the bundle; the hub rewrites it to `camstack-local://`. */
10794
+ url: string().min(1),
10795
+ sizeMB: number().nonnegative(),
10796
+ isDirectory: boolean().optional(),
10797
+ files: array(string()).optional(),
10798
+ runtimes: array(literal("python")).optional(),
10799
+ revision: string().min(1)
10800
+ });
10801
+ /** The trainer's half of a catalog entry; the hub completes it (labels, licence, group). */
10802
+ var TrainingCatalogDraftSchema = object({
10803
+ id: string().min(1),
10804
+ name: string().min(1),
10805
+ description: string(),
10806
+ labels: array(unknown()),
10807
+ inputSize: object({
10808
+ width: number().int().positive(),
10809
+ height: number().int().positive()
10810
+ }),
10811
+ preprocessMode: literal("letterbox"),
10812
+ defaultClassFloors: FloorsSchema,
10813
+ formats: object({
10814
+ onnx: DraftFormatSchema.optional(),
10815
+ openvino: DraftFormatSchema.optional(),
10816
+ coreml: DraftFormatSchema.optional()
10817
+ })
10818
+ });
10819
+ /** One class of one model's holdout report (the eval harness's summary). */
10820
+ var TrainingEvalClassSchema = object({
10821
+ positives: number().int(),
10822
+ floor: number().nullable(),
10823
+ tp: number().int(),
10824
+ fp: number().int(),
10825
+ fn: number().int(),
10826
+ precision: number().nullable(),
10827
+ recall: number().nullable(),
10828
+ ap50: number().nullable(),
10829
+ recallByCamera: record(string(), number().nullable()).optional(),
10830
+ recallByLight: record(string(), number().nullable()).optional(),
10831
+ fpByCamera: record(string(), number()).optional(),
10832
+ smallFound: number().int().optional(),
10833
+ smallTotal: number().int().optional()
10834
+ }).passthrough();
10835
+ var TrainingEvalReportSchema = object({
10836
+ frames: number().int(),
10837
+ classes: record(string(), TrainingEvalClassSchema)
10838
+ }).passthrough();
10839
+ /**
10840
+ * A succeeded result carries every block; a failed one carries the error and
10841
+ * whatever was known by then — the other blocks are ABSENT, not null. The
10842
+ * orchestrator checks the blocks it needs before it trusts a success.
10843
+ */
10844
+ var TrainingResultSchema = object({
10845
+ schema: literal(TRAINING_RESULT_SCHEMA),
10846
+ jobKey: string(),
10847
+ status: _enum(["succeeded", "failed"]),
10848
+ error: object({
10849
+ stage: string(),
10850
+ code: string(),
10851
+ message: string()
10852
+ }).nullable(),
10853
+ trainer: object({
10854
+ version: string(),
10855
+ revision: string()
10856
+ }),
10857
+ toolchain: record(string(), string().nullable()),
10858
+ dataset: object({
10859
+ versionId: string(),
10860
+ annotationHash: string(),
10861
+ sha256: string(),
10862
+ frames: object({
10863
+ train: number().int(),
10864
+ val: number().int(),
10865
+ holdout: number().int()
10866
+ }),
10867
+ holdoutCameras: array(number().int()),
10868
+ replayImages: number().int()
10869
+ }).optional(),
10870
+ training: object({
10871
+ epochsRun: number().int(),
10872
+ bestEpoch: number().int(),
10873
+ bestMap50: number(),
10874
+ bestMap50_95: number(),
10875
+ seconds: number()
10876
+ }).optional(),
10877
+ evaluation: object({
10878
+ holdoutFrames: number().int(),
10879
+ baseline: TrainingEvalReportSchema.nullable(),
10880
+ candidate: TrainingEvalReportSchema.nullable(),
10881
+ candidateInt8: TrainingEvalReportSchema.nullable(),
10882
+ recallDeltaByCamera: record(string(), record(string(), number())).nullable(),
10883
+ map50: record(string(), number().nullable())
10884
+ }).optional(),
10885
+ floors: object({
10886
+ baseline: FloorsSchema,
10887
+ candidate: FloorsSchema,
10888
+ candidateInt8: FloorsSchema.nullable(),
10889
+ notes: array(string())
10890
+ }).optional(),
10891
+ int8: object({
10892
+ kept: boolean(),
10893
+ map50Fp16: number().nullable(),
10894
+ map50Int8: number().nullable(),
10895
+ reason: string().nullable()
10896
+ }).optional(),
10897
+ exportsDropped: array(object({
10898
+ export: string(),
10899
+ reason: string()
10900
+ })).optional(),
10901
+ files: array(TrainingResultFileSchema).optional(),
10902
+ bundle: object({
10903
+ path: literal("bundle.tar"),
10904
+ bytes: number().int().nonnegative(),
10905
+ sha256: string().regex(/^[0-9a-f]{64}$/)
10906
+ }).nullable().optional(),
10907
+ catalogDraft: object({
10908
+ base: TrainingCatalogDraftSchema,
10909
+ int8: TrainingCatalogDraftSchema.nullable()
10910
+ }).nullable().optional(),
10911
+ licence: string()
10912
+ });
10913
+ var TrainingStageSchema = _enum([
10914
+ "prepare",
10915
+ "replay",
10916
+ "train",
10917
+ "eval",
10918
+ "export",
10919
+ "package"
10920
+ ]);
10921
+ var EventBase = {
10922
+ v: literal(1),
10923
+ ts: number()
10924
+ };
10925
+ discriminatedUnion("type", [
10926
+ object({
10927
+ ...EventBase,
10928
+ type: literal("stage"),
10929
+ stage: TrainingStageSchema
10930
+ }),
10931
+ object({
10932
+ ...EventBase,
10933
+ type: literal("epoch"),
10934
+ epoch: number().int(),
10935
+ epochs: number().int(),
10936
+ boxLoss: number().nullable().optional(),
10937
+ map50: number().nullable().optional(),
10938
+ map50_95: number().nullable().optional(),
10939
+ etaSec: number().nullable().optional()
10940
+ }),
10941
+ object({
10942
+ ...EventBase,
10943
+ type: literal("metric"),
10944
+ name: string(),
10945
+ value: number()
10946
+ }),
10947
+ object({
10948
+ ...EventBase,
10949
+ type: literal("artifact"),
10950
+ path: string(),
10951
+ bytes: number(),
10952
+ sha256: string()
10953
+ }),
10954
+ object({
10955
+ ...EventBase,
10956
+ type: literal("done"),
10957
+ status: _enum(["succeeded", "failed"]),
10958
+ resultSha256: string().nullable().optional()
10959
+ }),
10960
+ object({
10961
+ ...EventBase,
10962
+ type: literal("error"),
10963
+ stage: string(),
10964
+ code: string(),
10965
+ message: string()
10966
+ })
10967
+ ]);
10968
+ /**
10274
10969
  * What the gate decided about a birth, from the CALLER's point of view.
10275
10970
  *
10276
10971
  * Deliberately not `ConfirmationVerdict`: `undecided` is not a birth decision
@@ -12598,7 +13293,9 @@ var AddonHttpRouteSchema = object({
12598
13293
  "authenticated",
12599
13294
  "admin"
12600
13295
  ]).optional(),
12601
- description: string().optional()
13296
+ description: string().optional(),
13297
+ /** The hub limits this route per client address before the handler runs. */
13298
+ rateLimit: _enum(["auth"]).optional()
12602
13299
  });
12603
13300
  /**
12604
13301
  * Cross-process route invocation envelope. The hub captures the
@@ -13303,6 +14000,37 @@ var BackupDestinationInfoSchema = object({
13303
14000
  lastError: string().optional()
13304
14001
  });
13305
14002
  /**
14003
+ * How an archive is encrypted (operator decision 2026-10-09): to a backup
14004
+ * PUBLIC key the hub holds, so only the operator's secret key opens it.
14005
+ * Absent on a plain `.tar.gz` — every archive made before encryption, or
14006
+ * with it off — which restores without a key.
14007
+ */
14008
+ var BackupArchiveEncryptionSchema = object({
14009
+ /** `x25519-hkdf-aes256gcm-stream-v1`. */
14010
+ scheme: string(),
14011
+ /** 16 hex — the backup key whose secret restores this archive. */
14012
+ keyId: string()
14013
+ });
14014
+ /**
14015
+ * The backup encryption setting. The hub keeps PUBLIC keys only: the active
14016
+ * one new archives are encrypted to, and the retired ones older archives
14017
+ * still need. The secret key is shown once, at `setupEncryption`.
14018
+ */
14019
+ var BackupEncryptionStatusSchema = object({
14020
+ enabled: boolean(),
14021
+ active: object({
14022
+ keyId: string(),
14023
+ publicKey: string(),
14024
+ createdAt: number()
14025
+ }).nullable(),
14026
+ /** Every key ever set up, newest first. */
14027
+ keys: array(object({
14028
+ keyId: string(),
14029
+ createdAt: number(),
14030
+ retiredAt: number().nullable()
14031
+ })).readonly()
14032
+ });
14033
+ /**
13306
14034
  * Per-archive entry returned by `backup.listArchives({ destinationId })`.
13307
14035
  * Same shape the destination drill-in renders. Sourced from the per-
13308
14036
  * location `manifests.json` (no per-archive tarball read required).
@@ -13314,7 +14042,9 @@ var BackupArchiveEntrySchema = object({
13314
14042
  sizeBytes: number(),
13315
14043
  label: string().optional(),
13316
14044
  /** Top-level locations included in the archive (db, addons, tls, …). */
13317
- locations: array(string()).readonly()
14045
+ locations: array(string()).readonly(),
14046
+ /** Present when the archive is encrypted; restore then needs that key. */
14047
+ encryption: BackupArchiveEncryptionSchema.optional()
13318
14048
  });
13319
14049
  var BackupEntrySchema = object({
13320
14050
  id: string(),
@@ -13323,7 +14053,8 @@ var BackupEntrySchema = object({
13323
14053
  label: string().optional(),
13324
14054
  createdAt: number(),
13325
14055
  sizeBytes: number(),
13326
- locations: array(string()).optional()
14056
+ locations: array(string()).optional(),
14057
+ encryption: BackupArchiveEncryptionSchema.optional()
13327
14058
  });
13328
14059
  var ArchiveEntrySchema = object({
13329
14060
  path: string(),
@@ -13512,10 +14243,40 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
13512
14243
  * this list and skips entries whose path doesn't start with
13513
14244
  * any of them.
13514
14245
  */
13515
- locations: array(string()).optional()
14246
+ locations: array(string()).optional(),
14247
+ /**
14248
+ * The backup SECRET key, for an encrypted archive (ignored for a plain
14249
+ * one). The archive is decrypted into the hub's private staging
14250
+ * before the restart; the key is not stored. A wrong or missing key
14251
+ * is refused before the archive is downloaded.
14252
+ */
14253
+ backupSecretKey: string().optional()
13516
14254
  }), _void(), {
13517
14255
  kind: "mutation",
13518
14256
  auth: "admin"
14257
+ }), method(_void(), BackupEncryptionStatusSchema, { auth: "admin" }), method(object({}), object({
14258
+ keyId: string(),
14259
+ secretKey: string(),
14260
+ publicKey: string(),
14261
+ createdAt: number(),
14262
+ replacedKeyId: string().nullable()
14263
+ }), {
14264
+ kind: "mutation",
14265
+ auth: "admin"
14266
+ }), method(object({}), object({ disabledKeyId: string().nullable() }), {
14267
+ kind: "mutation",
14268
+ auth: "admin"
14269
+ }), method(object({ backupSecretKey: string() }), object({
14270
+ valid: boolean(),
14271
+ keyId: string().nullable(),
14272
+ /** The hub knows this key (active or retired). */
14273
+ known: boolean(),
14274
+ /** It is the key new archives are encrypted to. */
14275
+ active: boolean(),
14276
+ error: string().optional()
14277
+ }), {
14278
+ kind: "mutation",
14279
+ auth: "admin"
13519
14280
  }), method(object({
13520
14281
  destinationId: string(),
13521
14282
  backupId: string()
@@ -15241,7 +16002,60 @@ var CustomModelDescriptorSchema = object({
15241
16002
  stepId: string(),
15242
16003
  entry: ModelCatalogEntrySchema
15243
16004
  });
15244
- method(_void(), array(CustomModelDescriptorSchema).readonly(), { auth: "admin" });
16005
+ /** One regular file of an imported bundle, as the producer measured it. */
16006
+ var ModelBundleFileSchema = object({
16007
+ /** Relative path inside the bundle tar; an mlpackage member is `<x>.mlpackage/<member>`. */
16008
+ path: string().min(1),
16009
+ bytes: number().int().nonnegative(),
16010
+ sha256: string().regex(/^[0-9a-f]{64}$/)
16011
+ });
16012
+ var ImportModelBundleInputSchema = object({
16013
+ /** The registry provider that keeps the model (Model Studio). */
16014
+ addonId: string(),
16015
+ stepId: string().min(1),
16016
+ /**
16017
+ * The catalog rows to register, complete, every format URL already
16018
+ * `camstack-local://<modelId>/<file>` naming a file of the bundle. One
16019
+ * bundle may carry several rows (a base and its INT8 variant).
16020
+ */
16021
+ entries: array(ModelCatalogEntrySchema).min(1).max(4),
16022
+ /** The bundle tar, offered by the producer over `ctx.peerBytes` (same host). */
16023
+ bundle: object({
16024
+ ticket: PeerBytesTicketSchema,
16025
+ bytes: number().int().positive(),
16026
+ sha256: string().regex(/^[0-9a-f]{64}$/)
16027
+ }),
16028
+ /** Every file the tar must hold; anything missing, extra or different is refused. */
16029
+ files: array(ModelBundleFileSchema).min(1).max(64),
16030
+ /** Convert missing engine formats and push the model to every agent afterwards. */
16031
+ distribute: boolean()
16032
+ });
16033
+ var ImportModelBundleResultSchema = discriminatedUnion("kind", [object({
16034
+ kind: literal("imported"),
16035
+ modelIds: array(string()).readonly(),
16036
+ distributed: array(object({
16037
+ modelId: string(),
16038
+ nodeId: string(),
16039
+ format: _enum(MODEL_FORMATS),
16040
+ ok: boolean(),
16041
+ error: string().optional()
16042
+ })).readonly()
16043
+ }), object({
16044
+ kind: literal("refused"),
16045
+ reason: _enum([
16046
+ "transfer-failed",
16047
+ "checksum-mismatch",
16048
+ "bundle-mismatch",
16049
+ "invalid-entry",
16050
+ "id-taken"
16051
+ ]),
16052
+ detail: string()
16053
+ })]);
16054
+ method(_void(), array(CustomModelDescriptorSchema).readonly(), { auth: "admin" }), method(ImportModelBundleInputSchema, ImportModelBundleResultSchema, {
16055
+ kind: "mutation",
16056
+ auth: "admin",
16057
+ timeoutMs: 30 * 6e4
16058
+ });
15245
16059
  /**
15246
16060
  * Query filter for settings-store collections.
15247
16061
  */
@@ -15720,6 +16534,35 @@ var KeychainKeySourceSchema = object({
15720
16534
  */
15721
16535
  sharesBackupWithDatabase: boolean()
15722
16536
  });
16537
+ /**
16538
+ * One keychain reset, as the Secrets page reports it until the operator
16539
+ * acknowledges it. Never a value: refs, counts and table names only.
16540
+ */
16541
+ var KeychainResetRecordSchema = object({
16542
+ id: string(),
16543
+ at: number(),
16544
+ /** `user:<id>` or {@link KEYCHAIN_RESET_TRIGGER_ACTOR}. */
16545
+ actor: string(),
16546
+ /** Why the keychain was locked when it was reset; `null` when it was not locked. */
16547
+ lockedReason: string().nullable(),
16548
+ /** Values moved into the archive table (ciphertext under the lost key). */
16549
+ archived: number().int(),
16550
+ /** `keychain:secrets-orphaned-<ts>`, or `null` when nothing was stored. */
16551
+ archivedCollection: string().nullable(),
16552
+ /**
16553
+ * `keychain:keys-orphaned-<ts>`: the wrapped data keys of the archive, kept
16554
+ * so the archive can still be opened if the lost master key turns up.
16555
+ */
16556
+ archivedKeysCollection: string().nullable(),
16557
+ /**
16558
+ * `system-settings` keys whose marker the reset cleared — the cluster
16559
+ * secret's: a marker left pointing at an archived value would refuse every
16560
+ * agent forever (D797).
16561
+ */
16562
+ clearedSettingsKeys: array(string()).readonly(),
16563
+ /** When an operator dismissed the notice on the Secrets page; `null` until then. */
16564
+ acknowledgedAt: number().nullable()
16565
+ });
15723
16566
  var KeychainStatusSchema = object({
15724
16567
  keychain: KeychainStateSchema,
15725
16568
  /** The source of the master key in use; `null` while locked. */
@@ -15737,7 +16580,9 @@ var KeychainStatusSchema = object({
15737
16580
  /** Wrapped copies of the data key under a previous master key, kept until purged. */
15738
16581
  retiredWraps: number().int(),
15739
16582
  recoveryKeyDownloadedAt: number().nullable(),
15740
- secretCount: number().int()
16583
+ secretCount: number().int(),
16584
+ /** The most recent reset, acknowledged or not; `null` when the keychain was never reset. */
16585
+ lastReset: KeychainResetRecordSchema.nullable()
15741
16586
  });
15742
16587
  /**
15743
16588
  * Whether a stored value is still pointed at by its owner's settings.
@@ -15759,17 +16604,26 @@ var KeychainAuditActionSchema = _enum([
15759
16604
  "download-recovery-key",
15760
16605
  "delete-orphans",
15761
16606
  "reset",
16607
+ "acknowledge-reset",
15762
16608
  "initialize"
15763
16609
  ]);
15764
16610
  var KeychainAuditEntrySchema = object({
15765
16611
  id: string(),
15766
16612
  at: number(),
15767
16613
  action: KeychainAuditActionSchema,
15768
- /** `addon:<id>` | `user:<id>` | `system`. */
16614
+ /** `addon:<id>` | `user:<id>` | `system` | `trigger-file`. */
15769
16615
  actor: string(),
15770
16616
  ref: string().nullable(),
15771
16617
  detail: string().nullable()
15772
16618
  });
16619
+ var KEYCHAIN_RESET_CONFIRMATION = "RESET KEYCHAIN";
16620
+ /**
16621
+ * Secrets the HUB itself holds (owner `system`) that no settings form shows:
16622
+ * `jwt-signing-key` — the session signing key (D797). It is always "in use",
16623
+ * never an orphan; it is generated on a fresh hub and replaced only through
16624
+ * {@link keychainAdminCapability.methods.setSystemSecret}.
16625
+ */
16626
+ var SystemHeldSecretNameSchema = _enum(["jwt-signing-key"]);
15773
16627
  method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}), array(KeychainAdminSecretSchema).readonly(), { auth: "admin" }), method(object({ limit: number().int().min(1).max(500).optional() }), array(KeychainAuditEntrySchema).readonly(), { auth: "admin" }), method(object({ ref: string().min(1) }), object({
15774
16628
  value: string(),
15775
16629
  version: number().int()
@@ -15777,6 +16631,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
15777
16631
  kind: "mutation",
15778
16632
  auth: "admin",
15779
16633
  caller: "required"
16634
+ }), method(object({
16635
+ name: SystemHeldSecretNameSchema,
16636
+ value: string().min(16)
16637
+ }), object({
16638
+ ref: string(),
16639
+ version: number().int()
16640
+ }), {
16641
+ kind: "mutation",
16642
+ auth: "admin",
16643
+ caller: "required"
15780
16644
  }), method(object({ refs: array(string().min(1)).max(1e3) }), object({
15781
16645
  deleted: array(string()).readonly(),
15782
16646
  kept: array(object({
@@ -15806,13 +16670,20 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
15806
16670
  kind: "mutation",
15807
16671
  auth: "admin",
15808
16672
  caller: "required"
15809
- }), method(object({ confirm: literal("RESET KEYCHAIN") }), object({
16673
+ }), method(object({ confirm: literal(KEYCHAIN_RESET_CONFIRMATION) }), object({
15810
16674
  archivedCollection: string().nullable(),
15811
16675
  archived: number().int()
15812
16676
  }), {
15813
16677
  kind: "mutation",
15814
16678
  auth: "admin",
15815
16679
  caller: "required"
16680
+ }), method(object({}), object({
16681
+ reset: KeychainResetRecordSchema.nullable(),
16682
+ pendingReentry: array(KeychainSecretMetaSchema).readonly()
16683
+ }), { auth: "admin" }), method(object({}), object({ acknowledged: boolean() }), {
16684
+ kind: "mutation",
16685
+ auth: "admin",
16686
+ caller: "required"
15816
16687
  });
15817
16688
  method(_void(), object({
15818
16689
  id: string(),
@@ -18663,7 +19534,7 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
18663
19534
  * `login-method` — collection cap through which auth addons contribute
18664
19535
  * their pre-auth login surfaces to the login page. This is the SINGLE,
18665
19536
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
18666
- * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
19537
+ * every auth addon (OIDC, WebAuthn/passkey) registers a
18667
19538
  * `login-method` provider and the PUBLIC `auth.listLoginMethods`
18668
19539
  * procedure aggregates them for the unauthenticated login page.
18669
19540
  *
@@ -18671,8 +19542,7 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
18671
19542
  *
18672
19543
  * - `redirect` — a declarative button. The login page renders a generic
18673
19544
  * button that navigates to `startUrl` (an addon-owned HTTP route).
18674
- * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
18675
- * ZERO shell-side JS. A future SSO addon plugs in the same way — the
19545
+ * Covers OIDC (`/addon/auth-oidc/<id>/start`) with ZERO shell-side JS. A future SSO addon plugs in the same way — the
18676
19546
  * login page needs NO change.
18677
19547
  *
18678
19548
  * - `widget` — a Module-Federation widget the login page mounts (via
@@ -18691,8 +19561,8 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
18691
19561
  * decision.
18692
19562
  *
18693
19563
  * Every contribution carries a `stage`:
18694
- * - `primary` — shown on the first credentials screen (OIDC /
18695
- * magic-link buttons; a future usernameless passkey).
19564
+ * - `primary` — shown on the first credentials screen (OIDC
19565
+ * buttons; a usernameless passkey).
18696
19566
  * - `second-factor` — shown AFTER the password leg, gated on the
18697
19567
  * returned `factors` (passkey-as-2FA today).
18698
19568
  *
@@ -18719,7 +19589,7 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
18719
19589
  * The provider names an external identity a signed-in user can link to
18720
19590
  * their own account (D782): `auth.listLinkableProviders` /
18721
19591
  * `beginExternalLink` offer exactly the redirect methods that set this,
18722
- * whatever their id. Absent / false — a login button only (magic-link).
19592
+ * whatever their id. Absent / false — a login button only.
18723
19593
  */
18724
19594
  linkable: boolean().optional()
18725
19595
  }),
@@ -22574,8 +23444,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
22574
23444
  * `super_admin` — collapsed onto the single boolean because every
22575
23445
  * non-admin caller is governed by the same scope-check rules; the
22576
23446
  * distinction was only ever used to route role-specific procedures
22577
- * which we no longer have. Agents are admin sessions issued via
22578
- * `createServiceToken`; CLI / webhook integrations are `cst_*` tokens
23447
+ * which we no longer have. CLI / webhook integrations are `cst_*` tokens
22579
23448
  * with `isAdmin: false` + an explicit scope list.
22580
23449
  */
22581
23450
  /** Per-method access flavour. Materialised on every cap method via codegen. */
@@ -24250,6 +25119,9 @@ var RetrainExportFilterSchema = object({
24250
25119
  frameIds: array(string()).max(2e4).optional(),
24251
25120
  /** Only frames in at least one of these named collections. */
24252
25121
  collectionIds: array(string()).max(200).optional(),
25122
+ /** Exactly the frames of this dataset version (and its completeOnly). An
25123
+ * unknown id selects nothing. */
25124
+ datasetVersionId: string().optional(),
24253
25125
  batchId: string().optional(),
24254
25126
  /** Only frames the operator marked complete (all subjects boxed). */
24255
25127
  completeOnly: boolean().optional()
@@ -24417,6 +25289,67 @@ var RetrainLlmAssistResultSchema = discriminatedUnion("kind", [object({
24417
25289
  ]),
24418
25290
  detail: string().optional()
24419
25291
  })]);
25292
+ /** What a dataset version is cut from. Every field narrows. */
25293
+ var RetrainDatasetSelectionSchema = object({
25294
+ deviceIds: array(number()).readonly().optional(),
25295
+ collectionIds: array(string()).max(200).readonly().optional(),
25296
+ frameIds: array(string()).max(2e4).readonly().optional()
25297
+ });
25298
+ /** The counts a dataset is judged by. */
25299
+ var RetrainDatasetStatsSchema = object({
25300
+ frames: number().int(),
25301
+ completeFrames: number().int(),
25302
+ /** `completeFrames / frames`, 0..1. */
25303
+ completeness: number(),
25304
+ /** Boxes to learn (model errors excluded). */
25305
+ subjects: number().int(),
25306
+ modelErrors: number().int(),
25307
+ /** Complete frames with no subject. */
25308
+ hardNegatives: number().int(),
25309
+ /** Subjects per class × camera × day/night. */
25310
+ cells: array(object({
25311
+ macroClass: string(),
25312
+ deviceId: number(),
25313
+ lighting: _enum([
25314
+ "day",
25315
+ "night",
25316
+ "unknown"
25317
+ ]),
25318
+ subjects: number().int(),
25319
+ frames: number().int()
25320
+ })).readonly(),
25321
+ cameras: array(object({
25322
+ deviceId: number(),
25323
+ frames: number().int(),
25324
+ completeFrames: number().int()
25325
+ })).readonly()
25326
+ });
25327
+ /** An immutable dataset version, without its frame list. */
25328
+ var RetrainDatasetVersionSchema = object({
25329
+ id: string(),
25330
+ name: string(),
25331
+ createdAt: number(),
25332
+ selection: RetrainDatasetSelectionSchema,
25333
+ layout: RetrainExportLayoutSchema,
25334
+ completeOnly: boolean(),
25335
+ frameCount: number().int(),
25336
+ /** sha256 of what the frames taught when the version was frozen. */
25337
+ annotationHash: string(),
25338
+ /** The export manifest schema the version was cut under. */
25339
+ manifestSchema: string(),
25340
+ stats: RetrainDatasetStatsSchema
25341
+ });
25342
+ var CreateRetrainDatasetVersionResultSchema = discriminatedUnion("kind", [object({
25343
+ kind: literal("created"),
25344
+ version: RetrainDatasetVersionSchema
25345
+ }), object({
25346
+ kind: literal("refused"),
25347
+ reason: _enum([
25348
+ "empty-name",
25349
+ "duplicate-name",
25350
+ "no-frames"
25351
+ ])
25352
+ })]);
24420
25353
  /** The outcome of a lifecycle move owned by the retrain page. */
24421
25354
  var RetrainTransitionResultSchema = object({
24422
25355
  trackId: string(),
@@ -25206,6 +26139,47 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
25206
26139
  kind: "mutation",
25207
26140
  auth: "admin",
25208
26141
  timeoutMs: 60 * 6e4
26142
+ }), method(object({
26143
+ filter: RetrainExportFilterSchema,
26144
+ layout: RetrainExportLayoutSchema
26145
+ }), object({
26146
+ ticket: PeerBytesTicketSchema,
26147
+ frames: number().int(),
26148
+ annotations: number().int()
26149
+ }), {
26150
+ kind: "mutation",
26151
+ auth: "admin",
26152
+ timeoutMs: 12e4
26153
+ }), method(object({
26154
+ selection: RetrainDatasetSelectionSchema,
26155
+ completeOnly: boolean().optional()
26156
+ }), RetrainDatasetStatsSchema, {
26157
+ kind: "query",
26158
+ auth: "admin"
26159
+ }), method(object({
26160
+ name: string().max(120),
26161
+ selection: RetrainDatasetSelectionSchema,
26162
+ layout: RetrainExportLayoutSchema,
26163
+ completeOnly: boolean()
26164
+ }), CreateRetrainDatasetVersionResultSchema, {
26165
+ kind: "mutation",
26166
+ auth: "admin",
26167
+ timeoutMs: 12e4
26168
+ }), method(object({}), array(RetrainDatasetVersionSchema).readonly(), {
26169
+ kind: "query",
26170
+ auth: "admin"
26171
+ }), method(object({ id: string() }), object({ deleted: boolean() }), {
26172
+ kind: "mutation",
26173
+ auth: "admin"
26174
+ }), method(object({ id: string() }), object({
26175
+ found: boolean(),
26176
+ annotationHash: string(),
26177
+ matches: boolean(),
26178
+ missingFrames: number().int()
26179
+ }), {
26180
+ kind: "query",
26181
+ auth: "admin",
26182
+ timeoutMs: 12e4
25209
26183
  }), method(object({}), array(RetrainAssistProfileSchema).readonly(), {
25210
26184
  kind: "query",
25211
26185
  auth: "admin"
@@ -28784,8 +29758,8 @@ version: string().optional() }), ServerUpdateActionResultSchema, {
28784
29758
  * `smtp-provider` — pluggable email delivery surface.
28785
29759
  *
28786
29760
  * Collection cap: a deployment may install multiple SMTP relays (e.g.
28787
- * one for transactional / one for notifications). Consumers (magic-link,
28788
- * notifier addons, …) walk the collection and pick the first
29761
+ * one for transactional / one for notifications). Consumers (notifier
29762
+ * addons, …) walk the collection and pick the first
28789
29763
  * ready provider OR target a specific addonId.
28790
29764
  *
28791
29765
  * The shape stays small on purpose — `sendEmail` is enough for the
@@ -28964,7 +29938,7 @@ targets: array(object({
28964
29938
  })));
28965
29939
  /**
28966
29940
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
28967
- * providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
29941
+ * providers (OIDC, SAML, …) mint an HMAC-signed token
28968
29942
  * carrying the post-callback claims before redirecting back to
28969
29943
  * `/api/auth/sso/finish`.
28970
29944
  *
@@ -29027,7 +30001,14 @@ var SsoBridgeClaimsSchema = object({
29027
30001
  * Verified `iss` of the id_token behind an OIDC login bridge (D782). A
29028
30002
  * subject is unique only per issuer, so a link is bound to it.
29029
30003
  */
29030
- issuer: string().optional()
30004
+ issuer: string().optional(),
30005
+ /**
30006
+ * SHA-256 (hex) of the `camstack_sso_state` cookie the IdP `/start` set in
30007
+ * the browser that began this sign-in. `/api/auth/sso/finish` requires the
30008
+ * request to present the matching cookie — a login bridge is useless in
30009
+ * any other browser.
30010
+ */
30011
+ browserBinding: string().optional()
29031
30012
  });
29032
30013
  method(object({
29033
30014
  claims: SsoBridgeClaimsSchema,
@@ -41294,16 +42275,6 @@ var ApiKeySummarySchema = object({
41294
42275
  createdAt: number(),
41295
42276
  lastUsedAt: number().optional()
41296
42277
  });
41297
- var CreateApiKeyInputSchema = object({
41298
- label: string(),
41299
- isAdmin: boolean().default(false),
41300
- allowedProviders: union([array(string()), literal("*")]).optional(),
41301
- allowedDevices: record(string(), union([array(string()), literal("*")])).optional()
41302
- });
41303
- var CreateApiKeyResultSchema = object({
41304
- token: string(),
41305
- record: ApiKeySummarySchema
41306
- });
41307
42278
  var ScopedTokenSummarySchema = object({
41308
42279
  id: string(),
41309
42280
  userId: string(),
@@ -41402,17 +42373,10 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
41402
42373
  }), UserSummarySchema.extend({ passwordHash: string() }).nullable(), {
41403
42374
  kind: "mutation",
41404
42375
  access: "view"
41405
- }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(CreateApiKeyInputSchema, CreateApiKeyResultSchema, {
41406
- kind: "mutation",
41407
- auth: "admin",
41408
- access: "create"
41409
- }), method(object({ id: string() }), object({ success: literal(true) }), {
42376
+ }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: literal(true) }), {
41410
42377
  kind: "mutation",
41411
42378
  auth: "admin",
41412
42379
  access: "delete"
41413
- }), method(object({ token: string() }), ApiKeySummarySchema.nullable(), {
41414
- kind: "mutation",
41415
- access: "view"
41416
42380
  }), method(CreateScopedTokenInputSchema, CreateScopedTokenResultSchema, {
41417
42381
  kind: "mutation",
41418
42382
  auth: "admin",
@@ -41486,15 +42450,7 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
41486
42450
  }).nullable(), { access: "view" }), method(object({ userId: string() }), array(ExternalIdentitySchema), { auth: "admin" }), method(ExternalIdentityKeySchema, object({
41487
42451
  userId: string(),
41488
42452
  issuer: string()
41489
- }).nullable(), { auth: "admin" }), method(ExternalIdentityKeySchema.extend({
41490
- userId: string(),
41491
- issuer: string().min(1),
41492
- label: string().optional()
41493
- }), object({ success: literal(true) }), {
41494
- kind: "mutation",
41495
- auth: "admin",
41496
- access: "create"
41497
- }), method(ExternalIdentityKeySchema.extend({ userId: string() }), object({ success: boolean() }), {
42453
+ }).nullable(), { auth: "admin" }), method(ExternalIdentityKeySchema.extend({ userId: string() }), object({ success: boolean() }), {
41498
42454
  kind: "mutation",
41499
42455
  auth: "admin",
41500
42456
  access: "delete"
@@ -42199,6 +43155,451 @@ var zoneRulesCapability = {
42199
43155
  */
42200
43156
  durability: "restored"
42201
43157
  };
43158
+ /**
43159
+ * `training-provider` — ONE cloud that can run the CamStack trainer.
43160
+ *
43161
+ * A thin, mockable translation layer: it knows how to run a container with a
43162
+ * GPU, move a dataset in, read the job's stdout and hand artefacts back. It
43163
+ * knows nothing about datasets, splits, floors, cost policy or Model Studio —
43164
+ * those are the `training` orchestrator's (D805). Every rule written there
43165
+ * once is what makes a second provider (RunPod, a local NVIDIA node) a new
43166
+ * manifest entry and nothing else.
43167
+ *
43168
+ * Collection, routed by the `addonId` in every single-row input (the `llm` /
43169
+ * `notification-output` posture); `describe` concat-fans across providers.
43170
+ * Every method is `admin`: these spend money.
43171
+ *
43172
+ * Every cloud object a provider creates is tagged with the orchestrator's
43173
+ * `jobKey`, so a retried submit finds the job it already started instead of
43174
+ * starting a second GPU, and `listJobs` can find orphans.
43175
+ *
43176
+ * Bytes never ride a cap field: the dataset arrives as a `ctx.peerBytes`
43177
+ * ticket the producer (pipeline-analytics) minted, and artefacts leave as a
43178
+ * ticket the provider mints (D613).
43179
+ */
43180
+ var TrainingGpuOptionSchema = object({
43181
+ id: string(),
43182
+ label: string(),
43183
+ /** List price, USD per GPU-hour, before any region multiplier. */
43184
+ usdPerHour: number().nonnegative(),
43185
+ vramGb: number().nonnegative()
43186
+ });
43187
+ var TrainingRegionOptionSchema = object({
43188
+ /** `''` = any region (the provider's choice). */
43189
+ id: string(),
43190
+ label: string(),
43191
+ priceMultiplier: number().positive()
43192
+ });
43193
+ /**
43194
+ * The provider's per-account settings, as the orchestrator reads them. They
43195
+ * live in the PROVIDER addon's settings (operator decision 2026-10-08: they
43196
+ * depend on the user's account), never hard-coded in the orchestrator.
43197
+ */
43198
+ var TrainingProviderDefaultsSchema = object({
43199
+ gpuType: string(),
43200
+ region: string(),
43201
+ costCapUsd: number().nonnegative(),
43202
+ maxRuntimeSec: number().int().positive(),
43203
+ /** Hours a FAILED job's cloud dataset is kept for debugging; 0 = delete at once. */
43204
+ failedDatasetGraceHours: number().nonnegative()
43205
+ });
43206
+ var TrainingProviderDescriptorSchema = object({
43207
+ addonId: string(),
43208
+ /** `modal`, `runpod`, `fake`, … */
43209
+ kind: string(),
43210
+ label: string(),
43211
+ gpuTypes: array(TrainingGpuOptionSchema).readonly(),
43212
+ regions: array(TrainingRegionOptionSchema).readonly(),
43213
+ maxDatasetBytes: number().int().positive(),
43214
+ maxRuntimeSec: number().int().positive(),
43215
+ freeTierNote: string().optional(),
43216
+ /** Where the data goes, in one sentence the confirm dialog repeats. */
43217
+ dataResidencyNote: string(),
43218
+ /** Credentials are present — never the values. */
43219
+ configured: boolean(),
43220
+ defaults: TrainingProviderDefaultsSchema
43221
+ });
43222
+ var TrainingCredentialCheckSchema = object({
43223
+ ok: boolean(),
43224
+ /** Workspace / account name, when the provider says. */
43225
+ account: string().optional(),
43226
+ reason: _enum([
43227
+ "missing",
43228
+ "invalid",
43229
+ "forbidden",
43230
+ "network",
43231
+ "quota"
43232
+ ]).optional(),
43233
+ detail: string().optional()
43234
+ });
43235
+ var ProviderJobStateSchema = _enum([
43236
+ "pending",
43237
+ "starting",
43238
+ "running",
43239
+ "succeeded",
43240
+ "failed",
43241
+ "cancelled",
43242
+ "lost"
43243
+ ]);
43244
+ var ProviderJobStatusSchema = object({
43245
+ jobKey: string(),
43246
+ providerJobId: string().optional(),
43247
+ state: ProviderJobStateSchema,
43248
+ startedAt: number().optional(),
43249
+ endedAt: number().optional(),
43250
+ exitCode: number().int().optional(),
43251
+ failureReason: string().optional(),
43252
+ gpuType: string(),
43253
+ region: string().optional(),
43254
+ billedSeconds: number().nonnegative(),
43255
+ costUsd: number().nonnegative(),
43256
+ /** True when `costUsd` is runtime × list price, not a billing answer. */
43257
+ costIsEstimate: boolean(),
43258
+ lastFetchedAt: number()
43259
+ });
43260
+ /**
43261
+ * The trainer the job runs, by SOURCE revision: the provider turns it into an
43262
+ * image its own way (Modal builds the published Dockerfile at that revision;
43263
+ * a registry-pull provider maps it to a tag). Recorded on the job and echoed
43264
+ * in `result.json`.
43265
+ */
43266
+ var TrainerSourceRefSchema = object({
43267
+ /** Hugging Face repo id, e.g. `camstack/camstack-trainer`. */
43268
+ repo: string().min(1),
43269
+ /** A commit sha (never a branch) — the job must be reproducible. */
43270
+ revision: string().regex(/^[0-9a-f]{7,64}$/)
43271
+ });
43272
+ var JobRef = {
43273
+ addonId: string(),
43274
+ jobKey: string().uuid()
43275
+ };
43276
+ var TrainingLogLineSchema = object({
43277
+ ts: number(),
43278
+ text: string()
43279
+ });
43280
+ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({ addonId: string() }), TrainingCredentialCheckSchema, {
43281
+ kind: "mutation",
43282
+ auth: "admin",
43283
+ timeoutMs: 3e4
43284
+ }), method(object({
43285
+ addonId: string(),
43286
+ gpuType: string(),
43287
+ region: string(),
43288
+ datasetBytes: number().int().nonnegative(),
43289
+ gpuSecondsLow: number().nonnegative(),
43290
+ gpuSecondsHigh: number().nonnegative()
43291
+ }), object({
43292
+ usdLow: number().nonnegative(),
43293
+ usdHigh: number().nonnegative(),
43294
+ notes: array(string()).readonly()
43295
+ }), { auth: "admin" }), method(object({
43296
+ ...JobRef,
43297
+ ticket: PeerBytesTicketSchema
43298
+ }), object({
43299
+ bytes: number().int().nonnegative(),
43300
+ sha256: string()
43301
+ }), {
43302
+ kind: "mutation",
43303
+ auth: "admin",
43304
+ timeoutMs: 120 * 6e4
43305
+ }), method(object({
43306
+ ...JobRef,
43307
+ trainer: TrainerSourceRefSchema,
43308
+ jobSpecJson: string().min(2).max(64e3),
43309
+ gpuType: string(),
43310
+ region: string(),
43311
+ maxRuntimeSec: number().int().positive()
43312
+ }), ProviderJobStatusSchema, {
43313
+ kind: "mutation",
43314
+ auth: "admin",
43315
+ timeoutMs: 30 * 6e4
43316
+ }), method(object(JobRef), ProviderJobStatusSchema, {
43317
+ auth: "admin",
43318
+ timeoutMs: 6e4
43319
+ }), method(object({
43320
+ ...JobRef,
43321
+ cursor: string().optional(),
43322
+ max: number().int().min(1).max(500)
43323
+ }), object({
43324
+ lines: array(TrainingLogLineSchema).readonly(),
43325
+ cursor: string()
43326
+ }), {
43327
+ auth: "admin",
43328
+ timeoutMs: 6e4
43329
+ }), method(object(JobRef), object({ ok: literal(true) }), {
43330
+ kind: "mutation",
43331
+ auth: "admin",
43332
+ timeoutMs: 6e4
43333
+ }), method(object(JobRef), object({
43334
+ found: boolean(),
43335
+ text: string().optional()
43336
+ }), {
43337
+ kind: "mutation",
43338
+ auth: "admin",
43339
+ timeoutMs: 5 * 6e4
43340
+ }), method(object({
43341
+ ...JobRef,
43342
+ path: _enum(["bundle.tar"])
43343
+ }), object({ ticket: PeerBytesTicketSchema }), {
43344
+ kind: "mutation",
43345
+ auth: "admin",
43346
+ timeoutMs: 5 * 6e4
43347
+ }), method(object(JobRef), object({ deleted: array(string()).readonly() }), {
43348
+ kind: "mutation",
43349
+ auth: "admin",
43350
+ timeoutMs: 5 * 6e4
43351
+ }), method(object({ addonId: string() }), array(ProviderJobStatusSchema).readonly(), {
43352
+ auth: "admin",
43353
+ timeoutMs: 2 * 6e4
43354
+ });
43355
+ /**
43356
+ * `training` — the cloud-training orchestrator (D805). Hub-only singleton,
43357
+ * provided by `addon-training`.
43358
+ *
43359
+ * It owns what every provider must agree on, written once:
43360
+ *
43361
+ * - **Dataset versions.** Taken over from pipeline-analytics (D796): the rows
43362
+ * live here (D808); pipeline-analytics still CUTS a version (it owns the corpus and
43363
+ * the export plan) and reaches this store through the five row methods
43364
+ * below, so the Retrain page and the export did not change.
43365
+ * - **Jobs.** A persisted state machine, a reconcile loop against the
43366
+ * provider, a cost guard, a daily orphan sweep and the retention rules.
43367
+ * - **The return flow.** Verified artefacts → Model Studio
43368
+ * (`custom-model-registry.importModelBundle`) → canary on cameras →
43369
+ * rollback. Nothing is ever registered automatically.
43370
+ *
43371
+ * Every method is `admin`.
43372
+ */
43373
+ /** A version WITH its frame list — what the export and a job read. */
43374
+ var TrainingDatasetVersionRowSchema = RetrainDatasetVersionSchema.omit({ frameCount: true }).extend({ frameIds: array(string()).max(2e4).readonly() });
43375
+ /** A base model a detector job can fine-tune, and the baseline it is judged against. */
43376
+ var TrainingBaseModelSchema = object({
43377
+ modelId: string(),
43378
+ label: string(),
43379
+ weights: string(),
43380
+ imgsz: number().int(),
43381
+ baselineOnnxUrl: string(),
43382
+ baselineFloors: record(string(), number()),
43383
+ /** The base the fleet runs most — the UI's default. */
43384
+ recommended: boolean()
43385
+ });
43386
+ var TrainingPresetSchema = _enum([
43387
+ "quick",
43388
+ "standard",
43389
+ "thorough"
43390
+ ]);
43391
+ var TrainingJobRequestSchema = object({
43392
+ datasetVersionId: string().min(1),
43393
+ /** The `training-provider` addon that runs it. */
43394
+ addonId: string().min(1),
43395
+ gpuType: string().min(1),
43396
+ region: string(),
43397
+ baseModelId: string().min(1),
43398
+ preset: TrainingPresetSchema,
43399
+ /** Freeze the backbone's first N layers; `null` = the preset's choice. */
43400
+ freeze: number().int().nonnegative().nullable(),
43401
+ /** Cameras held out whole for evaluation; `null` = the orchestrator's default. */
43402
+ holdoutCameras: array(number().int()).nullable()
43403
+ });
43404
+ var TrainingSplitPlanSchema = object({
43405
+ holdoutCameras: array(number().int()).readonly(),
43406
+ /** Used when no camera is held out: the last N days of every camera. */
43407
+ holdoutLastDays: number().int().nonnegative(),
43408
+ /** Holdout subjects per class (camera holdout only; `null` = known only after the split). */
43409
+ holdoutSupport: record(string(), number().int()).nullable(),
43410
+ trainSubjects: number().int()
43411
+ });
43412
+ var TrainingRefusalReasonSchema = _enum([
43413
+ "unknown-dataset",
43414
+ "unknown-provider",
43415
+ "provider-not-configured",
43416
+ "unknown-base-model",
43417
+ "unknown-gpu",
43418
+ "dataset-too-large",
43419
+ "no-training-subjects",
43420
+ "job-already-running"
43421
+ ]);
43422
+ var TrainingJobPlanSchema = object({
43423
+ refusal: object({
43424
+ reason: TrainingRefusalReasonSchema,
43425
+ detail: string()
43426
+ }).nullable(),
43427
+ warnings: array(string()).readonly(),
43428
+ split: TrainingSplitPlanSchema.nullable(),
43429
+ estimate: object({
43430
+ datasetBytes: number().int(),
43431
+ uploadMinutesAt20Mbps: number(),
43432
+ gpuSecondsLow: number(),
43433
+ gpuSecondsHigh: number(),
43434
+ usdLow: number(),
43435
+ usdHigh: number(),
43436
+ notes: array(string()).readonly()
43437
+ }).nullable(),
43438
+ /** Pre-filled from the provider's settings; the operator may lower or raise them. */
43439
+ defaults: object({
43440
+ maxCostUsd: number(),
43441
+ maxRuntimeSec: number().int()
43442
+ }).nullable(),
43443
+ /** The sentence the confirm dialog repeats: where the frames go and when they are deleted. */
43444
+ dataNotice: string()
43445
+ });
43446
+ /**
43447
+ * Orchestrator-owned states, persisted. `uploading` covers exporting the
43448
+ * archive (it is streamed, never staged). Terminal: `failed`, `cancelled`,
43449
+ * `lost`, `expired`, `discarded`. `registered` and `canary` are where a
43450
+ * result lives after the operator acted on it.
43451
+ */
43452
+ var TrainingJobStateSchema = _enum([
43453
+ "uploading",
43454
+ "submitted",
43455
+ "starting",
43456
+ "running",
43457
+ "collecting",
43458
+ "ready",
43459
+ "registered",
43460
+ "canary",
43461
+ "failed",
43462
+ "cancelling",
43463
+ "cancelled",
43464
+ "lost",
43465
+ "expired",
43466
+ "discarded"
43467
+ ]);
43468
+ var TrainingFailureReasonSchema = _enum([
43469
+ "cost-cap",
43470
+ "runtime-cap",
43471
+ "upload-failed",
43472
+ "submit-failed",
43473
+ "trainer-failed",
43474
+ "result-invalid",
43475
+ "checksum-mismatch",
43476
+ "fetch-failed",
43477
+ "provider-error",
43478
+ "hub-restart-during-upload"
43479
+ ]);
43480
+ var TrainingProgressPointSchema = object({
43481
+ epoch: number().int(),
43482
+ map50: number().nullable(),
43483
+ map50_95: number().nullable()
43484
+ });
43485
+ var TrainingCanarySchema = object({
43486
+ deviceIds: array(number().int()).readonly(),
43487
+ since: number(),
43488
+ /** Every override the canary replaced, exactly, so rollback restores it. */
43489
+ previous: array(object({
43490
+ deviceId: number().int(),
43491
+ nodeId: string(),
43492
+ deviceKey: string(),
43493
+ patch: object({
43494
+ modelId: string().optional(),
43495
+ settings: record(string(), unknown()).readonly().optional()
43496
+ }).nullable()
43497
+ })).readonly()
43498
+ });
43499
+ var TrainingJobSchema = object({
43500
+ id: string(),
43501
+ idempotencyKey: string(),
43502
+ name: string(),
43503
+ createdAt: number(),
43504
+ updatedAt: number(),
43505
+ state: TrainingJobStateSchema,
43506
+ stage: TrainingStageSchema.nullable(),
43507
+ failure: object({
43508
+ reason: TrainingFailureReasonSchema,
43509
+ stage: string().nullable(),
43510
+ detail: string()
43511
+ }).nullable(),
43512
+ request: TrainingJobRequestSchema,
43513
+ maxCostUsd: number().nonnegative(),
43514
+ maxRuntimeSec: number().int().positive(),
43515
+ trainer: TrainerSourceRefSchema,
43516
+ datasetLayout: RetrainExportLayoutSchema,
43517
+ dataset: object({
43518
+ bytes: number().int(),
43519
+ sha256: string(),
43520
+ uploadedAt: number()
43521
+ }).nullable(),
43522
+ split: TrainingSplitPlanSchema,
43523
+ provider: object({
43524
+ state: ProviderJobStateSchema.nullable(),
43525
+ providerJobId: string().nullable(),
43526
+ startedAt: number().nullable(),
43527
+ endedAt: number().nullable(),
43528
+ billedSeconds: number(),
43529
+ costUsd: number(),
43530
+ costIsEstimate: boolean(),
43531
+ lastPolledAt: number().nullable()
43532
+ }),
43533
+ progress: object({
43534
+ epoch: number().int().nullable(),
43535
+ epochs: number().int().nullable(),
43536
+ etaSec: number().nullable(),
43537
+ series: array(TrainingProgressPointSchema).readonly()
43538
+ }),
43539
+ /** The last lines of the job's output (≤ 200), newest last. Never frame paths. */
43540
+ logTail: array(string()).readonly(),
43541
+ logCursor: string().nullable(),
43542
+ artifacts: object({
43543
+ bundleBytes: number().int(),
43544
+ bundleSha256: string(),
43545
+ fetchedAt: number(),
43546
+ /** When the staged artefacts are deleted from the hub. */
43547
+ expiresAt: number()
43548
+ }).nullable(),
43549
+ /** When the provider-side copy of the frames was deleted; null while it exists. */
43550
+ cloudDatasetDeletedAt: number().nullable(),
43551
+ /** A failed job's dataset is kept until then (the provider's grace), then deleted. */
43552
+ cloudCleanupDueAt: number().nullable(),
43553
+ /** Model ids registered from this job (base, and int8 when kept). */
43554
+ registeredModelIds: array(string()).readonly(),
43555
+ canary: TrainingCanarySchema.nullable()
43556
+ });
43557
+ var IdInput = object({ id: string().min(1) });
43558
+ method(TrainingDatasetVersionRowSchema, object({ stored: boolean() }), {
43559
+ kind: "mutation",
43560
+ auth: "admin"
43561
+ }), 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() }), {
43562
+ kind: "mutation",
43563
+ auth: "admin"
43564
+ }), method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" }), method(TrainingJobRequestSchema, TrainingJobPlanSchema, {
43565
+ auth: "admin",
43566
+ timeoutMs: 6e4
43567
+ }), method(TrainingJobRequestSchema.extend({
43568
+ name: string().min(1).max(120),
43569
+ maxCostUsd: number().positive(),
43570
+ maxRuntimeSec: number().int().positive(),
43571
+ idempotencyKey: string().min(8).max(80)
43572
+ }), discriminatedUnion("kind", [object({
43573
+ kind: literal("started"),
43574
+ job: TrainingJobSchema
43575
+ }), object({
43576
+ kind: literal("refused"),
43577
+ reason: TrainingRefusalReasonSchema,
43578
+ detail: string()
43579
+ })]), {
43580
+ kind: "mutation",
43581
+ auth: "admin",
43582
+ timeoutMs: 12e4
43583
+ }), method(object({}), array(TrainingJobSchema).readonly(), { auth: "admin" }), method(IdInput, TrainingJobSchema.nullable(), { auth: "admin" }), method(IdInput, TrainingJobSchema, {
43584
+ kind: "mutation",
43585
+ auth: "admin",
43586
+ timeoutMs: 12e4
43587
+ }), method(IdInput, TrainingResultSchema.nullable(), { auth: "admin" }), method(IdInput.extend({ canaryDeviceIds: array(number().int()).readonly() }), TrainingJobSchema, {
43588
+ kind: "mutation",
43589
+ auth: "admin",
43590
+ timeoutMs: 30 * 6e4
43591
+ }), method(IdInput.extend({ deviceIds: array(number().int()).min(1).readonly() }), TrainingJobSchema, {
43592
+ kind: "mutation",
43593
+ auth: "admin",
43594
+ timeoutMs: 12e4
43595
+ }), method(IdInput, TrainingJobSchema, {
43596
+ kind: "mutation",
43597
+ auth: "admin",
43598
+ timeoutMs: 12e4
43599
+ }), method(IdInput, TrainingJobSchema, {
43600
+ kind: "mutation",
43601
+ auth: "admin"
43602
+ });
42202
43603
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
42203
43604
  Object.freeze({
42204
43605
  person: .5,
@@ -44347,6 +45748,12 @@ Object.freeze({
44347
45748
  addonId: null,
44348
45749
  access: "create"
44349
45750
  },
45751
+ "backup.checkBackupKey": {
45752
+ capName: "backup",
45753
+ capScope: "system",
45754
+ addonId: null,
45755
+ access: "create"
45756
+ },
44350
45757
  "backup.delete": {
44351
45758
  capName: "backup",
44352
45759
  capScope: "system",
@@ -44359,6 +45766,18 @@ Object.freeze({
44359
45766
  addonId: null,
44360
45767
  access: "delete"
44361
45768
  },
45769
+ "backup.disableEncryption": {
45770
+ capName: "backup",
45771
+ capScope: "system",
45772
+ addonId: null,
45773
+ access: "create"
45774
+ },
45775
+ "backup.getEncryption": {
45776
+ capName: "backup",
45777
+ capScope: "system",
45778
+ addonId: null,
45779
+ access: "view"
45780
+ },
44362
45781
  "backup.getEntries": {
44363
45782
  capName: "backup",
44364
45783
  capScope: "system",
@@ -44413,6 +45832,12 @@ Object.freeze({
44413
45832
  addonId: null,
44414
45833
  access: "create"
44415
45834
  },
45835
+ "backup.setupEncryption": {
45836
+ capName: "backup",
45837
+ capScope: "system",
45838
+ addonId: null,
45839
+ access: "create"
45840
+ },
44416
45841
  "backup.trigger": {
44417
45842
  capName: "backup",
44418
45843
  capScope: "system",
@@ -44965,6 +46390,12 @@ Object.freeze({
44965
46390
  addonId: null,
44966
46391
  access: "create"
44967
46392
  },
46393
+ "customModelRegistry.importModelBundle": {
46394
+ capName: "custom-model-registry",
46395
+ capScope: "system",
46396
+ addonId: null,
46397
+ access: "create"
46398
+ },
44968
46399
  "customModelRegistry.listModels": {
44969
46400
  capName: "custom-model-registry",
44970
46401
  capScope: "system",
@@ -46405,6 +47836,12 @@ Object.freeze({
46405
47836
  addonId: null,
46406
47837
  access: "view"
46407
47838
  },
47839
+ "keychainAdmin.acknowledgeReset": {
47840
+ capName: "keychain-admin",
47841
+ capScope: "system",
47842
+ addonId: null,
47843
+ access: "create"
47844
+ },
46408
47845
  "keychainAdmin.deleteOrphans": {
46409
47846
  capName: "keychain-admin",
46410
47847
  capScope: "system",
@@ -46441,6 +47878,12 @@ Object.freeze({
46441
47878
  addonId: null,
46442
47879
  access: "delete"
46443
47880
  },
47881
+ "keychainAdmin.resetReport": {
47882
+ capName: "keychain-admin",
47883
+ capScope: "system",
47884
+ addonId: null,
47885
+ access: "delete"
47886
+ },
46444
47887
  "keychainAdmin.reveal": {
46445
47888
  capName: "keychain-admin",
46446
47889
  capScope: "system",
@@ -46453,6 +47896,12 @@ Object.freeze({
46453
47896
  addonId: null,
46454
47897
  access: "create"
46455
47898
  },
47899
+ "keychainAdmin.setSystemSecret": {
47900
+ capName: "keychain-admin",
47901
+ capScope: "system",
47902
+ addonId: null,
47903
+ access: "create"
47904
+ },
46456
47905
  "keychainAdmin.status": {
46457
47906
  capName: "keychain-admin",
46458
47907
  capScope: "system",
@@ -47899,6 +49348,12 @@ Object.freeze({
47899
49348
  addonId: null,
47900
49349
  access: "create"
47901
49350
  },
49351
+ "pipelineAnalytics.createRetrainDatasetVersion": {
49352
+ capName: "pipeline-analytics",
49353
+ capScope: "device",
49354
+ addonId: null,
49355
+ access: "create"
49356
+ },
47902
49357
  "pipelineAnalytics.deleteDeviceEvents": {
47903
49358
  capName: "pipeline-analytics",
47904
49359
  capScope: "device",
@@ -47911,6 +49366,12 @@ Object.freeze({
47911
49366
  addonId: null,
47912
49367
  access: "delete"
47913
49368
  },
49369
+ "pipelineAnalytics.deleteRetrainDatasetVersion": {
49370
+ capName: "pipeline-analytics",
49371
+ capScope: "device",
49372
+ addonId: null,
49373
+ access: "delete"
49374
+ },
47914
49375
  "pipelineAnalytics.deleteTracks": {
47915
49376
  capName: "pipeline-analytics",
47916
49377
  capScope: "device",
@@ -48133,6 +49594,12 @@ Object.freeze({
48133
49594
  addonId: null,
48134
49595
  access: "view"
48135
49596
  },
49597
+ "pipelineAnalytics.listRetrainDatasetVersions": {
49598
+ capName: "pipeline-analytics",
49599
+ capScope: "device",
49600
+ addonId: null,
49601
+ access: "view"
49602
+ },
48136
49603
  "pipelineAnalytics.listRetrainFrames": {
48137
49604
  capName: "pipeline-analytics",
48138
49605
  capScope: "device",
@@ -48163,6 +49630,12 @@ Object.freeze({
48163
49630
  addonId: null,
48164
49631
  access: "view"
48165
49632
  },
49633
+ "pipelineAnalytics.offerRetrainArchive": {
49634
+ capName: "pipeline-analytics",
49635
+ capScope: "device",
49636
+ addonId: null,
49637
+ access: "create"
49638
+ },
48166
49639
  "pipelineAnalytics.ownersWithMedia": {
48167
49640
  capName: "pipeline-analytics",
48168
49641
  capScope: "device",
@@ -48175,6 +49648,12 @@ Object.freeze({
48175
49648
  addonId: null,
48176
49649
  access: "create"
48177
49650
  },
49651
+ "pipelineAnalytics.previewRetrainDataset": {
49652
+ capName: "pipeline-analytics",
49653
+ capScope: "device",
49654
+ addonId: null,
49655
+ access: "view"
49656
+ },
48178
49657
  "pipelineAnalytics.proposeRetrainAnnotations": {
48179
49658
  capName: "pipeline-analytics",
48180
49659
  capScope: "device",
@@ -48313,6 +49792,12 @@ Object.freeze({
48313
49792
  addonId: null,
48314
49793
  access: "create"
48315
49794
  },
49795
+ "pipelineAnalytics.verifyRetrainDatasetVersion": {
49796
+ capName: "pipeline-analytics",
49797
+ capScope: "device",
49798
+ addonId: null,
49799
+ access: "view"
49800
+ },
48316
49801
  "pipelineAnalytics.wipeAllAnalytics": {
48317
49802
  capName: "pipeline-analytics",
48318
49803
  capScope: "device",
@@ -50689,6 +52174,174 @@ Object.freeze({
50689
52174
  addonId: null,
50690
52175
  access: "view"
50691
52176
  },
52177
+ "training.canaryResult": {
52178
+ capName: "training",
52179
+ capScope: "system",
52180
+ addonId: null,
52181
+ access: "create"
52182
+ },
52183
+ "training.cancelJob": {
52184
+ capName: "training",
52185
+ capScope: "system",
52186
+ addonId: null,
52187
+ access: "create"
52188
+ },
52189
+ "training.discardResult": {
52190
+ capName: "training",
52191
+ capScope: "system",
52192
+ addonId: null,
52193
+ access: "create"
52194
+ },
52195
+ "training.findDatasetVersionIdByName": {
52196
+ capName: "training",
52197
+ capScope: "system",
52198
+ addonId: null,
52199
+ access: "view"
52200
+ },
52201
+ "training.getDatasetVersion": {
52202
+ capName: "training",
52203
+ capScope: "system",
52204
+ addonId: null,
52205
+ access: "view"
52206
+ },
52207
+ "training.getJob": {
52208
+ capName: "training",
52209
+ capScope: "system",
52210
+ addonId: null,
52211
+ access: "view"
52212
+ },
52213
+ "training.getJobResult": {
52214
+ capName: "training",
52215
+ capScope: "system",
52216
+ addonId: null,
52217
+ access: "view"
52218
+ },
52219
+ "training.listBaseModels": {
52220
+ capName: "training",
52221
+ capScope: "system",
52222
+ addonId: null,
52223
+ access: "view"
52224
+ },
52225
+ "training.listDatasetVersions": {
52226
+ capName: "training",
52227
+ capScope: "system",
52228
+ addonId: null,
52229
+ access: "view"
52230
+ },
52231
+ "training.listJobs": {
52232
+ capName: "training",
52233
+ capScope: "system",
52234
+ addonId: null,
52235
+ access: "view"
52236
+ },
52237
+ "training.planJob": {
52238
+ capName: "training",
52239
+ capScope: "system",
52240
+ addonId: null,
52241
+ access: "view"
52242
+ },
52243
+ "training.putDatasetVersion": {
52244
+ capName: "training",
52245
+ capScope: "system",
52246
+ addonId: null,
52247
+ access: "create"
52248
+ },
52249
+ "training.registerResult": {
52250
+ capName: "training",
52251
+ capScope: "system",
52252
+ addonId: null,
52253
+ access: "create"
52254
+ },
52255
+ "training.removeDatasetVersion": {
52256
+ capName: "training",
52257
+ capScope: "system",
52258
+ addonId: null,
52259
+ access: "delete"
52260
+ },
52261
+ "training.rollbackCanary": {
52262
+ capName: "training",
52263
+ capScope: "system",
52264
+ addonId: null,
52265
+ access: "create"
52266
+ },
52267
+ "training.startJob": {
52268
+ capName: "training",
52269
+ capScope: "system",
52270
+ addonId: null,
52271
+ access: "create"
52272
+ },
52273
+ "trainingProvider.cancelJob": {
52274
+ capName: "training-provider",
52275
+ capScope: "system",
52276
+ addonId: null,
52277
+ access: "create"
52278
+ },
52279
+ "trainingProvider.cleanupJob": {
52280
+ capName: "training-provider",
52281
+ capScope: "system",
52282
+ addonId: null,
52283
+ access: "create"
52284
+ },
52285
+ "trainingProvider.describe": {
52286
+ capName: "training-provider",
52287
+ capScope: "system",
52288
+ addonId: null,
52289
+ access: "view"
52290
+ },
52291
+ "trainingProvider.estimateJob": {
52292
+ capName: "training-provider",
52293
+ capScope: "system",
52294
+ addonId: null,
52295
+ access: "view"
52296
+ },
52297
+ "trainingProvider.getJob": {
52298
+ capName: "training-provider",
52299
+ capScope: "system",
52300
+ addonId: null,
52301
+ access: "view"
52302
+ },
52303
+ "trainingProvider.listJobEvents": {
52304
+ capName: "training-provider",
52305
+ capScope: "system",
52306
+ addonId: null,
52307
+ access: "view"
52308
+ },
52309
+ "trainingProvider.listJobs": {
52310
+ capName: "training-provider",
52311
+ capScope: "system",
52312
+ addonId: null,
52313
+ access: "view"
52314
+ },
52315
+ "trainingProvider.offerArtifact": {
52316
+ capName: "training-provider",
52317
+ capScope: "system",
52318
+ addonId: null,
52319
+ access: "create"
52320
+ },
52321
+ "trainingProvider.readResult": {
52322
+ capName: "training-provider",
52323
+ capScope: "system",
52324
+ addonId: null,
52325
+ access: "create"
52326
+ },
52327
+ "trainingProvider.submitJob": {
52328
+ capName: "training-provider",
52329
+ capScope: "system",
52330
+ addonId: null,
52331
+ access: "create"
52332
+ },
52333
+ "trainingProvider.uploadDataset": {
52334
+ capName: "training-provider",
52335
+ capScope: "system",
52336
+ addonId: null,
52337
+ access: "create"
52338
+ },
52339
+ "trainingProvider.validateCredentials": {
52340
+ capName: "training-provider",
52341
+ capScope: "system",
52342
+ addonId: null,
52343
+ access: "create"
52344
+ },
50692
52345
  "turnProvider.getTurnServers": {
50693
52346
  capName: "turn-provider",
50694
52347
  capScope: "system",
@@ -50713,12 +52366,6 @@ Object.freeze({
50713
52366
  addonId: null,
50714
52367
  access: "create"
50715
52368
  },
50716
- "userManagement.createApiKey": {
50717
- capName: "user-management",
50718
- capScope: "system",
50719
- addonId: null,
50720
- access: "create"
50721
- },
50722
52369
  "userManagement.createScopedToken": {
50723
52370
  capName: "user-management",
50724
52371
  capScope: "system",
@@ -50755,12 +52402,6 @@ Object.freeze({
50755
52402
  addonId: null,
50756
52403
  access: "view"
50757
52404
  },
50758
- "userManagement.linkExternalIdentity": {
50759
- capName: "user-management",
50760
- capScope: "system",
50761
- addonId: null,
50762
- access: "create"
50763
- },
50764
52405
  "userManagement.listApiKeys": {
50765
52406
  capName: "user-management",
50766
52407
  capScope: "system",
@@ -50863,12 +52504,6 @@ Object.freeze({
50863
52504
  addonId: null,
50864
52505
  access: "create"
50865
52506
  },
50866
- "userManagement.validateApiKey": {
50867
- capName: "user-management",
50868
- capScope: "system",
50869
- addonId: null,
50870
- access: "view"
50871
- },
50872
52507
  "userManagement.validateCredentials": {
50873
52508
  capName: "user-management",
50874
52509
  capScope: "system",
@@ -54982,6 +56617,11 @@ Object.freeze({
54982
56617
  form: "single",
54983
56618
  optional: false
54984
56619
  }],
56620
+ "training.canaryResult": [{
56621
+ name: "deviceIds",
56622
+ form: "array",
56623
+ optional: false
56624
+ }],
54985
56625
  "update.getStatus": [{
54986
56626
  name: "deviceId",
54987
56627
  form: "single",