@camstack/types 1.2.331 → 1.2.333

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 (51) hide show
  1. package/dist/addon/base-addon.d.ts +32 -0
  2. package/dist/addon/secret-aware-context.d.ts +49 -6
  3. package/dist/addon/secret-settings-view.d.ts +23 -1
  4. package/dist/addon/secret-write-back.d.ts +40 -0
  5. package/dist/addon.js +5 -7
  6. package/dist/addon.mjs +5 -7
  7. package/dist/auth/app-callback.d.ts +28 -0
  8. package/dist/auth/sso-state.d.ts +16 -0
  9. package/dist/capabilities/addon-routes.cap.d.ts +3 -0
  10. package/dist/capabilities/addons.cap.d.ts +4 -4
  11. package/dist/capabilities/backup.cap.d.ts +101 -1
  12. package/dist/capabilities/broker.cap.d.ts +4 -4
  13. package/dist/capabilities/custom-model-registry.cap.d.ts +520 -0
  14. package/dist/capabilities/device-manager.cap.d.ts +4 -4
  15. package/dist/capabilities/hub-address.d.ts +2 -2
  16. package/dist/capabilities/index.d.ts +12 -6
  17. package/dist/capabilities/keychain-admin.cap.d.ts +83 -0
  18. package/dist/capabilities/login-method.cap.d.ts +5 -6
  19. package/dist/capabilities/notification-output.cap.d.ts +4 -4
  20. package/dist/capabilities/notification-rules.cap.d.ts +6 -6
  21. package/dist/capabilities/osd-manager.cap.d.ts +5 -5
  22. package/dist/capabilities/pipeline-analytics.cap.d.ts +39 -8
  23. package/dist/capabilities/recording.cap.d.ts +3 -3
  24. package/dist/capabilities/server-management.cap.d.ts +5 -5
  25. package/dist/capabilities/snapshot.cap.d.ts +2 -2
  26. package/dist/capabilities/sso-bridge.cap.d.ts +5 -2
  27. package/dist/capabilities/training-provider.cap.d.ts +373 -0
  28. package/dist/capabilities/training.cap.d.ts +1896 -0
  29. package/dist/capabilities/user-management.cap.d.ts +1 -64
  30. package/dist/capabilities/videoclips.cap.d.ts +3 -3
  31. package/dist/client.js +26 -4
  32. package/dist/client.mjs +26 -4
  33. package/dist/generated/addon-api.d.ts +842 -173
  34. package/dist/generated/cap-input-defaults.d.ts +1 -1
  35. package/dist/generated/capability-router-map.d.ts +10 -4
  36. package/dist/generated/collection-array-methods.d.ts +1 -1
  37. package/dist/generated/method-access-map.d.ts +1 -1
  38. package/dist/generated/method-device-selectors.d.ts +2 -2
  39. package/dist/generated/system-proxy.d.ts +5 -3
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.js +1462 -93
  42. package/dist/index.mjs +1413 -92
  43. package/dist/interfaces/addon-routes.d.ts +7 -0
  44. package/dist/interfaces/addon.d.ts +8 -0
  45. package/dist/interfaces/auth.d.ts +6 -3
  46. package/dist/schemas/auth-records.d.ts +1 -2
  47. package/dist/{sleep-ClNXPpWR.js → sleep-DiQYrfm9.js} +425 -75
  48. package/dist/{sleep-DujUvOcb.mjs → sleep-r3RWkGVx.mjs} +425 -75
  49. package/dist/training/index.d.ts +1 -0
  50. package/dist/training/trainer-contract.d.ts +468 -0
  51. package/package.json +1 -1
@@ -162,7 +162,7 @@ function collectKeychainStoredKeys(schema) {
162
162
  return keys;
163
163
  }
164
164
  function walkKeychainField(field, out) {
165
- if (!isRecord$2(field)) return;
165
+ if (!isRecord$3(field)) return;
166
166
  const type = field["type"];
167
167
  const key = field["key"];
168
168
  const isSecret = type === "password" || field["secret"] === true;
@@ -187,21 +187,21 @@ function schemaDeclaresAnyField(schema) {
187
187
  for (const section of sectionsOf(schema)) if (fieldsOf(section).length > 0) return true;
188
188
  return false;
189
189
  }
190
- function isRecord$2(value) {
190
+ function isRecord$3(value) {
191
191
  return typeof value === "object" && value !== null && !Array.isArray(value);
192
192
  }
193
193
  function sectionsOf(schema) {
194
- if (!isRecord$2(schema)) return [];
194
+ if (!isRecord$3(schema)) return [];
195
195
  const sections = schema["sections"];
196
196
  return Array.isArray(sections) ? sections : [];
197
197
  }
198
198
  function fieldsOf(node) {
199
- if (!isRecord$2(node)) return [];
199
+ if (!isRecord$3(node)) return [];
200
200
  const fields = node["fields"];
201
201
  return Array.isArray(fields) ? fields : [];
202
202
  }
203
203
  function walkField(field, out) {
204
- if (!isRecord$2(field)) return;
204
+ if (!isRecord$3(field)) return;
205
205
  const type = field["type"];
206
206
  const key = field["key"];
207
207
  if ((type === "password" || field["secret"] === true) && typeof key === "string" && key.length > 0) out.add(key);
@@ -241,7 +241,7 @@ function walkField(field, out) {
241
241
  var SECRET_CLEAR = { $secretClear: true };
242
242
  /** The `'***'` token D634 redaction writes for non-admins. */
243
243
  var LEGACY_DEVICE_REDACTION = "***";
244
- function isRecord$1(value) {
244
+ function isRecord$2(value) {
245
245
  return typeof value === "object" && value !== null && !Array.isArray(value);
246
246
  }
247
247
  function makeSecretMarker(ref) {
@@ -252,19 +252,19 @@ function makeSecretMarker(ref) {
252
252
  }
253
253
  /** A stored marker: an object whose `$secret` is a non-empty string. */
254
254
  function isSecretMarker(value) {
255
- if (!isRecord$1(value)) return false;
255
+ if (!isRecord$2(value)) return false;
256
256
  const ref = value["$secret"];
257
257
  return typeof ref === "string" && ref.length > 0;
258
258
  }
259
259
  /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
260
260
  function isSecretMarkerLike(value) {
261
- return isRecord$1(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
261
+ return isRecord$2(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
262
262
  }
263
263
  function isSecretClear(value) {
264
- return isRecord$1(value) && value["$secretClear"] === true;
264
+ return isRecord$2(value) && value["$secretClear"] === true;
265
265
  }
266
266
  function isSecretFieldView(value) {
267
- return isRecord$1(value) && value["set"] === true && isSecretMarkerLike(value);
267
+ return isRecord$2(value) && value["set"] === true && isSecretMarkerLike(value);
268
268
  }
269
269
  /**
270
270
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -1018,7 +1018,7 @@ var KeychainWriteRefusedError = class extends Error {
1018
1018
  }
1019
1019
  };
1020
1020
  var KEEP = { kind: "keep" };
1021
- function isRecord(value) {
1021
+ function isRecord$1(value) {
1022
1022
  return typeof value === "object" && value !== null && !Array.isArray(value);
1023
1023
  }
1024
1024
  function defaultMintId() {
@@ -1031,7 +1031,7 @@ function collectSecretMarkerRefs(value, out = []) {
1031
1031
  return out;
1032
1032
  }
1033
1033
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
1034
- else if (isRecord(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
1034
+ else if (isRecord$1(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
1035
1035
  return out;
1036
1036
  }
1037
1037
  async function storedValueEquals(secrets, marker, candidate) {
@@ -1086,7 +1086,7 @@ function rowId(row, idKey) {
1086
1086
  return null;
1087
1087
  }
1088
1088
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
1089
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord) : [];
1089
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$1) : [];
1090
1090
  if (!Array.isArray(incoming)) {
1091
1091
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
1092
1092
  return {
@@ -1102,7 +1102,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
1102
1102
  const kept = /* @__PURE__ */ new Set();
1103
1103
  const rows = [];
1104
1104
  for (const row of incoming) {
1105
- if (!isRecord(row)) {
1105
+ if (!isRecord$1(row)) {
1106
1106
  rows.push(row);
1107
1107
  continue;
1108
1108
  }
@@ -1127,7 +1127,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
1127
1127
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
1128
1128
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
1129
1129
  case "object": {
1130
- if (!isRecord(incoming)) {
1130
+ if (!isRecord$1(incoming)) {
1131
1131
  if (incoming === void 0) return KEEP;
1132
1132
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
1133
1133
  return {
@@ -1135,7 +1135,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
1135
1135
  value: incoming
1136
1136
  };
1137
1137
  }
1138
- const storedObj = isRecord(stored) ? stored : {};
1138
+ const storedObj = isRecord$1(stored) ? stored : {};
1139
1139
  return {
1140
1140
  kind: "value",
1141
1141
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -1194,12 +1194,19 @@ async function transformSecretWrite(input) {
1194
1194
  }
1195
1195
  };
1196
1196
  }
1197
+ /** Is `ref` one of `owner`'s refs? */
1198
+ function isSecretRefOwnedBy(ref, owner) {
1199
+ return parseSecretRef(ref)?.owner === owner;
1200
+ }
1197
1201
  /**
1198
1202
  * Replace every marker in `value` with its keychain value. Returns the SAME
1199
1203
  * reference when there is no marker (no allocation, no keychain call).
1200
1204
  */
1201
- async function resolveSecretMarkers(value, secrets) {
1202
- const refs = collectSecretMarkerRefs(value);
1205
+ async function resolveSecretMarkers(value, secrets, options = {}) {
1206
+ const keepMarker = options.unresolved === "keep-marker";
1207
+ const owner = options.ownedBy;
1208
+ const inScope = (ref) => owner === void 0 || isSecretRefOwnedBy(ref, owner);
1209
+ const refs = collectSecretMarkerRefs(value).filter(inScope);
1203
1210
  if (refs.length === 0) return {
1204
1211
  value,
1205
1212
  unresolved: []
@@ -1223,10 +1230,18 @@ async function resolveSecretMarkers(value, secrets) {
1223
1230
  }
1224
1231
  const substitute = (v) => {
1225
1232
  if (isSecretMarker(v)) {
1233
+ if (!inScope(v.$secret)) return {
1234
+ keep: true,
1235
+ value: v
1236
+ };
1226
1237
  const answer = answers[v.$secret];
1227
- return answer?.kind === "value" ? {
1238
+ if (answer?.kind === "value") return {
1228
1239
  keep: true,
1229
1240
  value: answer.value
1241
+ };
1242
+ return keepMarker ? {
1243
+ keep: true,
1244
+ value: v
1230
1245
  } : {
1231
1246
  keep: false,
1232
1247
  value: void 0
@@ -1243,7 +1258,7 @@ async function resolveSecretMarkers(value, secrets) {
1243
1258
  value: items
1244
1259
  };
1245
1260
  }
1246
- if (isRecord(v)) {
1261
+ if (isRecord$1(v)) {
1247
1262
  const out = {};
1248
1263
  for (const [k, child] of Object.entries(v)) {
1249
1264
  const s = substitute(child);
@@ -1265,15 +1280,155 @@ async function resolveSecretMarkers(value, secrets) {
1265
1280
  };
1266
1281
  }
1267
1282
  //#endregion
1268
- //#region src/addon/secret-aware-context.ts
1269
- function touchesTree(tree, patch) {
1270
- if (tree.size === 0) return false;
1271
- for (const key of Object.keys(patch)) {
1272
- const at = key.indexOf("@");
1273
- if (tree.has(at > 0 ? key.slice(0, at) : key)) return true;
1283
+ //#region src/addon/secret-write-back.ts
1284
+ /** An addon tried to replace a stored secret it could not read. Nothing was written. */
1285
+ var SecretOverwriteRefusedError = class extends Error {
1286
+ ref;
1287
+ reason;
1288
+ constructor(ref, reason) {
1289
+ 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.`);
1290
+ this.ref = ref;
1291
+ this.reason = reason;
1292
+ this.name = "SecretOverwriteRefusedError";
1293
+ }
1294
+ };
1295
+ function isRecord(value) {
1296
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1297
+ }
1298
+ function resolvedValue(pass, marker) {
1299
+ const answer = pass.answers[marker.$secret];
1300
+ return answer?.kind === "value" ? answer.value : null;
1301
+ }
1302
+ async function atMarker(pass, incoming, stored) {
1303
+ if (incoming === null) return stored;
1304
+ if (typeof incoming !== "string") return incoming;
1305
+ if (incoming === resolvedValue(pass, stored)) return stored;
1306
+ const intent = classifySecretWrite(incoming);
1307
+ if (intent.kind !== "set") return stored;
1308
+ const ref = stored.$secret;
1309
+ if (!isSecretRefOwnedBy(ref, pass.secrets.owner)) return incoming;
1310
+ const answer = pass.answers[ref];
1311
+ 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);
1312
+ try {
1313
+ await pass.secrets.put(ref, intent.value);
1314
+ } catch (err) {
1315
+ throw new KeychainWriteRefusedError(ref, err);
1316
+ }
1317
+ pass.changedRefs.push(ref);
1318
+ return stored;
1319
+ }
1320
+ function inArray(pass, incoming, stored) {
1321
+ return incoming.map((item, index) => {
1322
+ const twin = stored[index];
1323
+ if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
1324
+ if (isRecord(item) && isRecord(twin)) return inArrayRow(pass, item, twin);
1325
+ if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
1326
+ return item;
1327
+ });
1328
+ }
1329
+ function inArrayRow(pass, incoming, stored) {
1330
+ const out = { ...incoming };
1331
+ for (const [key, value] of Object.entries(incoming)) {
1332
+ const twin = stored[key];
1333
+ if (isSecretMarker(twin)) {
1334
+ if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
1335
+ } else if (isRecord(value) && isRecord(twin)) out[key] = inArrayRow(pass, value, twin);
1336
+ else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
1337
+ }
1338
+ return out;
1339
+ }
1340
+ async function walk(pass, incoming, stored) {
1341
+ if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
1342
+ if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
1343
+ if (!isRecord(incoming) || !isRecord(stored)) return incoming;
1344
+ const out = { ...incoming };
1345
+ for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
1346
+ return out;
1347
+ }
1348
+ /** Restore markers a write-back carries as values. See the file doc. */
1349
+ async function restoreUnchangedSecrets(input) {
1350
+ const refs = [];
1351
+ for (const key of Object.keys(input.patch)) collectSecretMarkerRefs(input.stored[key], refs);
1352
+ const owned = refs.filter((ref) => isSecretRefOwnedBy(ref, input.secrets.owner));
1353
+ if (owned.length === 0) return {
1354
+ patch: input.patch,
1355
+ changedRefs: []
1356
+ };
1357
+ const pass = {
1358
+ secrets: input.secrets,
1359
+ answers: await input.secrets.resolve([...new Set(owned)]),
1360
+ operatorWrite: input.operatorWrite,
1361
+ changedRefs: []
1362
+ };
1363
+ const out = {};
1364
+ for (const [key, value] of Object.entries(input.patch)) out[key] = await walk(pass, value, input.stored[key]);
1365
+ return {
1366
+ patch: out,
1367
+ changedRefs: pass.changedRefs
1368
+ };
1369
+ }
1370
+ /** Structural equality over JSON-shaped values (key order does not matter). */
1371
+ function isJsonEqual(a, b) {
1372
+ if (Object.is(a, b)) return true;
1373
+ if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
1374
+ if (isRecord(a) && isRecord(b)) {
1375
+ const keys = Object.keys(a);
1376
+ if (keys.length !== Object.keys(b).length) return false;
1377
+ return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
1274
1378
  }
1275
1379
  return false;
1276
1380
  }
1381
+ /** The patch without the keys whose value already equals the stored one. */
1382
+ function dropUnchangedKeys(patch, stored) {
1383
+ const out = {};
1384
+ for (const [key, value] of Object.entries(patch)) {
1385
+ if (Object.prototype.hasOwnProperty.call(stored, key) && isJsonEqual(value, stored[key])) continue;
1386
+ out[key] = value;
1387
+ }
1388
+ return out;
1389
+ }
1390
+ /** The top-level keys of a blob that hold at least one of `owner`'s markers. */
1391
+ function markerKeysOf(blob, owner) {
1392
+ const keys = /* @__PURE__ */ new Set();
1393
+ for (const [key, value] of Object.entries(blob)) if (collectSecretMarkerRefs(value).some((ref) => isSecretRefOwnedBy(ref, owner))) keys.add(key);
1394
+ return keys;
1395
+ }
1396
+ function nodeFromStored(value, owner) {
1397
+ if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
1398
+ if (!isRecord(value)) return null;
1399
+ const children = /* @__PURE__ */ new Map();
1400
+ for (const [key, child] of Object.entries(value)) {
1401
+ const node = nodeFromStored(child, owner);
1402
+ if (node !== null) children.set(key, node);
1403
+ }
1404
+ return children.size > 0 ? {
1405
+ kind: "object",
1406
+ children
1407
+ } : null;
1408
+ }
1409
+ /**
1410
+ * The declared secret tree, widened by every place the STORED blob holds a
1411
+ * marker under one of `keys` — so a marker the schema no longer declares (or
1412
+ * never declared: Wyze before b4906cec6) still gets the save path's
1413
+ * KEEP / SET / CLEAR semantics instead of being overwritten by whatever the
1414
+ * addon writes. Markers inside arrays are not added: a row has no id field to
1415
+ * match by unless the schema declares one. Only `owner`'s markers count: a
1416
+ * builtin that keeps another addon's marker (device-manager) never takes it
1417
+ * over.
1418
+ */
1419
+ function widenTreeWithStoredMarkers(declared, stored, keys, owner) {
1420
+ const added = /* @__PURE__ */ new Map();
1421
+ for (const key of keys) {
1422
+ const at = key.indexOf("@");
1423
+ const base = at > 0 ? key.slice(0, at) : key;
1424
+ if (declared.has(base) || added.has(base)) continue;
1425
+ const node = nodeFromStored(stored[key], owner);
1426
+ if (node !== null) added.set(base, node);
1427
+ }
1428
+ return added.size === 0 ? declared : new Map([...declared, ...added]);
1429
+ }
1430
+ //#endregion
1431
+ //#region src/addon/secret-aware-context.ts
1277
1432
  /** The blob as stored now; a failed read REFUSES the write instead of guessing. */
1278
1433
  async function readStoredAddonBlob(raw) {
1279
1434
  const threeWay = raw.readAddonStoreResult;
@@ -1282,7 +1437,41 @@ async function readStoredAddonBlob(raw) {
1282
1437
  if (result.kind === "failed") throw new Error(`settings write refused: the stored blob could not be read (${result.error})`);
1283
1438
  return result.kind === "missing" ? {} : result.value;
1284
1439
  }
1285
- function createSecretAwareSettings(raw, opts) {
1440
+ function touchesTree(tree, patch) {
1441
+ if (tree.size === 0) return false;
1442
+ for (const key of Object.keys(patch)) {
1443
+ const at = key.indexOf("@");
1444
+ if (tree.has(at > 0 ? key.slice(0, at) : key)) return true;
1445
+ }
1446
+ return false;
1447
+ }
1448
+ /**
1449
+ * Which top-level keys of each store held markers when last seen. `undefined`
1450
+ * for a store not seen yet: its first write reads it, to be safe.
1451
+ */
1452
+ var MarkerKeyIndex = class {
1453
+ owner;
1454
+ byStore = /* @__PURE__ */ new Map();
1455
+ constructor(owner) {
1456
+ this.owner = owner;
1457
+ }
1458
+ seen(store, blob) {
1459
+ this.byStore.set(store, markerKeysOf(blob, this.owner));
1460
+ }
1461
+ wrote(store, patch) {
1462
+ const keys = this.byStore.get(store);
1463
+ if (keys === void 0) return;
1464
+ const now = markerKeysOf(patch, this.owner);
1465
+ for (const key of Object.keys(patch)) if (now.has(key)) keys.add(key);
1466
+ else keys.delete(key);
1467
+ }
1468
+ mayHoldMarkers(store, patch) {
1469
+ const keys = this.byStore.get(store);
1470
+ if (keys === void 0) return true;
1471
+ return Object.keys(patch).some((key) => keys.has(key));
1472
+ }
1473
+ };
1474
+ function createHandle(raw, opts) {
1286
1475
  const chains = /* @__PURE__ */ new Map();
1287
1476
  const serialize = (key, fn) => {
1288
1477
  const next = (chains.get(key) ?? Promise.resolve()).then(fn, fn);
@@ -1293,46 +1482,130 @@ function createSecretAwareSettings(raw, opts) {
1293
1482
  });
1294
1483
  return next;
1295
1484
  };
1296
- const write = async (chainKey, tree, scope, patch, readStored, rawWrite) => {
1297
- if (!touchesTree(tree, patch)) return rawWrite(patch);
1298
- return serialize(chainKey, async () => {
1485
+ const index = new MarkerKeyIndex(opts.secrets.owner);
1486
+ const resolveRead = async (store, blob) => {
1487
+ index.seen(store, blob);
1488
+ const { value, unresolved } = await resolveSecretMarkers(blob, opts.secrets, {
1489
+ unresolved: "keep-marker",
1490
+ ownedBy: opts.secrets.owner
1491
+ });
1492
+ if (unresolved.length > 0) opts.onUnresolved?.(unresolved);
1493
+ return value;
1494
+ };
1495
+ const write = async (store, declared, scope, patch, readStored, rawWrite) => {
1496
+ if (!touchesTree(declared, patch) && !index.mayHoldMarkers(store, patch)) {
1497
+ await rawWrite(patch);
1498
+ index.wrote(store, patch);
1499
+ return { changed: true };
1500
+ }
1501
+ return serialize(store, async () => {
1299
1502
  const stored = await readStored();
1503
+ index.seen(store, stored);
1504
+ const operatorWrite = opts.isOperatorWrite?.() === true;
1505
+ const restored = await restoreUnchangedSecrets({
1506
+ patch,
1507
+ stored,
1508
+ secrets: opts.secrets,
1509
+ operatorWrite
1510
+ });
1300
1511
  const result = await transformSecretWrite({
1301
- tree,
1512
+ tree: widenTreeWithStoredMarkers(declared, stored, Object.keys(restored.patch), opts.secrets.owner),
1302
1513
  secrets: opts.secrets,
1303
- patch,
1514
+ patch: restored.patch,
1304
1515
  stored,
1305
1516
  ...scope !== void 0 ? { scope } : {},
1306
- operatorWrite: opts.isOperatorWrite?.() === true
1517
+ operatorWrite
1307
1518
  });
1308
- if (Object.keys(result.patch).length > 0) await rawWrite(result.patch);
1519
+ const effective = dropUnchangedKeys(result.patch, stored);
1520
+ const wroteBlob = Object.keys(effective).length > 0;
1521
+ if (wroteBlob) {
1522
+ await rawWrite(effective);
1523
+ index.wrote(store, effective);
1524
+ }
1309
1525
  const { orphaned } = await result.afterWrite();
1310
1526
  if (orphaned.length > 0) opts.onOrphaned(orphaned);
1527
+ return { changed: wroteBlob || restored.changedRefs.length > 0 };
1311
1528
  });
1312
1529
  };
1313
- const wrapped = {
1314
- writeAddonStore: (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p)),
1315
- writeDeviceStore: (deviceId, patch) => write(`device:${deviceId}`, opts.deviceTree(), {
1316
- kind: "device",
1317
- deviceId
1318
- }, patch, () => raw.readDeviceStore(deviceId), (p) => raw.writeDeviceStore(deviceId, p))
1319
- };
1320
- return new Proxy(raw, { get(target, prop, receiver) {
1321
- if (prop === "writeAddonStore") return wrapped.writeAddonStore;
1322
- if (prop === "writeDeviceStore") return wrapped.writeDeviceStore;
1323
- const value = Reflect.get(target, prop, receiver);
1324
- return typeof value === "function" ? value.bind(target) : value;
1325
- } });
1326
- }
1327
- /** The context the addon sees: identical, except that its settings writes are secret-aware. */
1328
- function withSecretAwareSettings(context, opts) {
1530
+ const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
1531
+ const readAddonStoreResult = raw.readAddonStoreResult;
1532
+ const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
1533
+ const readDeviceStoreBatch = raw.readDeviceStoreBatch;
1534
+ const overrides = {
1535
+ readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
1536
+ ...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
1537
+ const result = await readAddonStoreResult.call(raw);
1538
+ return result.kind === "value" ? {
1539
+ kind: "value",
1540
+ value: await resolveRead("addon", result.value)
1541
+ } : result;
1542
+ } } : {},
1543
+ ...readAddonStoreKeyResult !== void 0 ? { readAddonStoreKeyResult: async (key) => {
1544
+ const result = await readAddonStoreKeyResult.call(raw, key);
1545
+ if (result.kind !== "present") return result;
1546
+ const { value, unresolved } = await resolveSecretMarkers({ [key]: result.value }, opts.secrets, {
1547
+ unresolved: "keep-marker",
1548
+ ownedBy: opts.secrets.owner
1549
+ });
1550
+ if (unresolved.length > 0) opts.onUnresolved?.(unresolved);
1551
+ return {
1552
+ kind: "present",
1553
+ value: value[key]
1554
+ };
1555
+ } } : {},
1556
+ readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
1557
+ ...readDeviceStoreBatch !== void 0 ? { readDeviceStoreBatch: async (deviceIds) => {
1558
+ const rows = await readDeviceStoreBatch.call(raw, deviceIds);
1559
+ const out = /* @__PURE__ */ new Map();
1560
+ for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
1561
+ return out;
1562
+ } } : {},
1563
+ writeAddonStore: async (patch) => {
1564
+ await writeAddon(patch);
1565
+ },
1566
+ writeDeviceStore: async (deviceId, patch) => {
1567
+ await write(`device:${deviceId}`, opts.deviceTree(), {
1568
+ kind: "device",
1569
+ deviceId
1570
+ }, patch, () => raw.readDeviceStore(deviceId), (p) => raw.writeDeviceStore(deviceId, p));
1571
+ }
1572
+ };
1573
+ return {
1574
+ settings: new Proxy(raw, { get(target, prop, receiver) {
1575
+ if (typeof prop === "string" && Object.prototype.hasOwnProperty.call(overrides, prop)) return Reflect.get(overrides, prop);
1576
+ const value = Reflect.get(target, prop, receiver);
1577
+ return typeof value === "function" ? value.bind(target) : value;
1578
+ } }),
1579
+ raw: {
1580
+ readAddonStore: async () => {
1581
+ const blob = await raw.readAddonStore();
1582
+ index.seen("addon", blob);
1583
+ return blob;
1584
+ },
1585
+ readDeviceStore: async (deviceId) => {
1586
+ const blob = await raw.readDeviceStore(deviceId);
1587
+ index.seen(`device:${deviceId}`, blob);
1588
+ return blob;
1589
+ }
1590
+ },
1591
+ writeAddonStore: writeAddon
1592
+ };
1593
+ }
1594
+ /** The context the addon sees, plus the private raw view and write outcome. */
1595
+ function createSecretAwareContext(context, opts) {
1329
1596
  const raw = context.settings;
1330
- if (raw === void 0) return context;
1331
- const settings = createSecretAwareSettings(raw, opts);
1332
- return new Proxy(context, { get(target, prop, receiver) {
1333
- if (prop === "settings") return settings;
1334
- return Reflect.get(target, prop, receiver);
1335
- } });
1597
+ if (raw === void 0) return {
1598
+ context,
1599
+ handle: null
1600
+ };
1601
+ const handle = createHandle(raw, opts);
1602
+ return {
1603
+ context: new Proxy(context, { get(target, prop, receiver) {
1604
+ if (prop === "settings") return handle.settings;
1605
+ return Reflect.get(target, prop, receiver);
1606
+ } }),
1607
+ handle
1608
+ };
1336
1609
  }
1337
1610
  //#endregion
1338
1611
  //#region src/addon/secret-paths.ts
@@ -1524,6 +1797,14 @@ var BaseAddon = class {
1524
1797
  _secretChangeUnsub = null;
1525
1798
  /** >0 while an OPERATOR's settings write is being applied. */
1526
1799
  _operatorWriteDepth = 0;
1800
+ /**
1801
+ * The keychain-aware settings handle (D798). Its `raw` view is the ONLY way
1802
+ * to the marker-bearing blobs and stays private to this class: the form
1803
+ * hydrate and config resolution need markers, addon code never does.
1804
+ */
1805
+ _secretSettings = null;
1806
+ /** Refs an addon-facing read could not resolve — each is logged once per process. */
1807
+ _loggedUnresolvedReads = /* @__PURE__ */ new Set();
1527
1808
  constructor(defaults) {
1528
1809
  this.defaults = defaults;
1529
1810
  this._config = { ...defaults };
@@ -1560,13 +1841,21 @@ var BaseAddon = class {
1560
1841
  }
1561
1842
  async initialize(context) {
1562
1843
  const secrets = context.secrets;
1563
- this._ctx = secrets !== void 0 ? withSecretAwareSettings(context, {
1564
- secrets,
1565
- addonTree: () => this.addonSecretTree(),
1566
- deviceTree: () => this.deviceSecretTree(),
1567
- onOrphaned: (refs) => context.logger.warn("keychain refs dropped by a settings write could not be deleted", { meta: { refs: [...refs] } }),
1568
- isOperatorWrite: () => this._operatorWriteDepth > 0
1569
- }) : context;
1844
+ if (secrets !== void 0) {
1845
+ const { context: wrapped, handle } = createSecretAwareContext(context, {
1846
+ secrets,
1847
+ addonTree: () => this.addonSecretTree(),
1848
+ deviceTree: () => this.deviceSecretTree(),
1849
+ onOrphaned: (refs) => context.logger.warn("keychain refs dropped by a settings write could not be deleted", { meta: { refs: [...refs] } }),
1850
+ isOperatorWrite: () => this._operatorWriteDepth > 0,
1851
+ onUnresolved: (unresolved) => this.logUnresolvedRead(unresolved)
1852
+ });
1853
+ this._ctx = wrapped;
1854
+ this._secretSettings = handle;
1855
+ } else {
1856
+ this._ctx = context;
1857
+ this._secretSettings = null;
1858
+ }
1570
1859
  await this.resolveConfig();
1571
1860
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
1572
1861
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -1683,11 +1972,8 @@ var BaseAddon = class {
1683
1972
  async getGlobalSettings(overlay, cap, nodeId) {
1684
1973
  const schema = this.globalSettingsSchema(cap);
1685
1974
  if (!schema) return { sections: [] };
1686
- const projected = await this.resolveGlobalStore(nodeId, cap);
1687
- return hydrateSchema(schema, overlay ? {
1688
- ...projected,
1689
- ...overlay
1690
- } : projected, { inlineSecretsAsViews: true });
1975
+ const projected = await this.readRawProjectedStore(nodeId, cap);
1976
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
1691
1977
  }
1692
1978
  /**
1693
1979
  * The integration-level view of this addon's settings: exactly the sections
@@ -1732,7 +2018,7 @@ var BaseAddon = class {
1732
2018
  });
1733
2019
  }
1734
2020
  if (sections.length === 0) return null;
1735
- const projected = await this.resolveGlobalStore(nodeId);
2021
+ const projected = await this.readRawProjectedStore(nodeId);
1736
2022
  return hydrateSchema({
1737
2023
  ...schema,
1738
2024
  sections
@@ -1749,12 +2035,50 @@ var BaseAddon = class {
1749
2035
  * the store for custom option logic (option narrowing, value snapping) to
1750
2036
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
1751
2037
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
2038
+ *
2039
+ * Keychain values come back RESOLVED (D798): a secret reads as its value,
2040
+ * never as a `{ $secret }` marker. One the keychain cannot serve right now
2041
+ * stays a marker — "stored, unavailable" (`isSecretMarker()` is true, and it
2042
+ * is listed in {@link unresolvedSecrets}) — never `''` and never absent, so
2043
+ * it cannot be mistaken for "nothing stored, mint a new one". Writing the
2044
+ * resolved blob back is a no-op for every unchanged secret.
1752
2045
  */
1753
2046
  async resolveGlobalStore(nodeId, cap) {
1754
2047
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
2048
+ return this.projectForNode(raw, nodeId, cap);
2049
+ }
2050
+ projectForNode(store, nodeId, cap) {
1755
2051
  const keys = this.perNodeKeys(cap);
1756
2052
  const node = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
1757
- return keys.size > 0 ? projectStore(raw, keys, node) : raw;
2053
+ return keys.size > 0 ? projectStore(store, keys, node) : store;
2054
+ }
2055
+ /** The PROJECTED store with its markers — the form hydrate only (D798). */
2056
+ async readRawProjectedStore(nodeId, cap) {
2057
+ const raw = await this.rawSettings()?.readAddonStore() ?? {};
2058
+ return this.projectForNode(raw, nodeId, cap);
2059
+ }
2060
+ /**
2061
+ * `{ ...stored, ...overlay }`, except that an overlay member equal to the
2062
+ * RESOLVED stored one keeps the stored (marker-bearing) member — so a form
2063
+ * hydrated from a resolved overlay still shows a secret as "set", never as
2064
+ * its value.
2065
+ */
2066
+ async overlayKeepingMarkers(stored, overlay) {
2067
+ const secrets = this._ctx?.secrets;
2068
+ const merged = {
2069
+ ...stored,
2070
+ ...overlay
2071
+ };
2072
+ if (secrets === void 0 || collectSecretMarkerRefs(stored).length === 0) return merged;
2073
+ const { value: resolved } = await resolveSecretMarkers(stored, secrets, {
2074
+ unresolved: "keep-marker",
2075
+ ownedBy: secrets.owner
2076
+ });
2077
+ for (const key of Object.keys(overlay)) {
2078
+ if (collectSecretMarkerRefs(stored[key]).length === 0) continue;
2079
+ if (isJsonEqual(overlay[key], resolved[key])) merged[key] = stored[key];
2080
+ }
2081
+ return merged;
1758
2082
  }
1759
2083
  /**
1760
2084
  * Whether ONE global key of this addon's store was written, read three-way
@@ -1819,8 +2143,12 @@ var BaseAddon = class {
1819
2143
  async clearSecrets(keys, nodeId) {
1820
2144
  const tree = this.addonSecretTree();
1821
2145
  const keychain = this._ctx?.secrets !== void 0;
2146
+ const stored = keychain ? await this.readAddonStoreWithRetry() : {};
1822
2147
  const patch = {};
1823
- for (const key of keys) patch[key] = keychain && tree.has(key) ? SECRET_CLEAR : "";
2148
+ for (const key of keys) {
2149
+ const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
2150
+ patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
2151
+ }
1824
2152
  await this.applyGlobalPatch(patch, nodeId);
1825
2153
  }
1826
2154
  async applyGlobalPatch(barePatch, nodeId) {
@@ -1828,7 +2156,11 @@ var BaseAddon = class {
1828
2156
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
1829
2157
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
1830
2158
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
1831
- await this._ctx?.settings?.writeAddonStore(scoped);
2159
+ const handle = this._secretSettings;
2160
+ if (handle !== null) {
2161
+ const { changed } = await handle.writeAddonStore(scoped);
2162
+ if (!changed) return;
2163
+ } else await this._ctx?.settings?.writeAddonStore(scoped);
1832
2164
  if (target !== localNode) return;
1833
2165
  await this.resolveConfig();
1834
2166
  await this.onConfigChanged();
@@ -1881,7 +2213,7 @@ var BaseAddon = class {
1881
2213
  async getDeviceSettings(deviceId) {
1882
2214
  const schema = this.deviceSettingsSchema();
1883
2215
  if (!schema) return { sections: [] };
1884
- return hydrateSchema(schema, await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, { inlineSecretsAsViews: true });
2216
+ return hydrateSchema(schema, await this.rawSettings()?.readDeviceStore(deviceId) ?? {}, { inlineSecretsAsViews: true });
1885
2217
  }
1886
2218
  async updateDeviceSettings(deviceId, patch) {
1887
2219
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
@@ -2053,7 +2385,7 @@ var BaseAddon = class {
2053
2385
  key,
2054
2386
  schema,
2055
2387
  fallback,
2056
- read: async () => this.resolveStoredSecrets(await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, false),
2388
+ read: async () => this.resolveStoredSecrets(await this.rawSettings()?.readDeviceStore(deviceId) ?? {}, false),
2057
2389
  write: async (patch) => {
2058
2390
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
2059
2391
  },
@@ -2115,6 +2447,23 @@ var BaseAddon = class {
2115
2447
  }
2116
2448
  return value;
2117
2449
  }
2450
+ /**
2451
+ * The marker-bearing settings reads (D798) — private on purpose. Without a
2452
+ * keychain the context's own view is already raw.
2453
+ */
2454
+ rawSettings() {
2455
+ return this._secretSettings?.raw ?? this._ctx?.settings;
2456
+ }
2457
+ /** An addon-facing read met markers it could not resolve: say so once per ref. */
2458
+ logUnresolvedRead(unresolved) {
2459
+ const fresh = unresolved.filter((u) => !this._loggedUnresolvedReads.has(u.ref));
2460
+ if (fresh.length === 0) return;
2461
+ for (const u of fresh) this._loggedUnresolvedReads.add(u.ref);
2462
+ 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) => ({
2463
+ ref: u.ref,
2464
+ reason: u.reason
2465
+ })) } });
2466
+ }
2118
2467
  /** Re-resolve the config (a keychain-only change, or a retry) and apply it. */
2119
2468
  scheduleSecretReresolve(delayMs) {
2120
2469
  const timerOf = delayMs === SECRET_CHANGE_DEBOUNCE_MS ? "_secretChangeTimer" : "_secretRetryTimer";
@@ -2141,7 +2490,7 @@ var BaseAddon = class {
2141
2490
  * `{}` (defaults) — the addon's first successful patch will rehydrate.
2142
2491
  */
2143
2492
  async readAddonStoreWithRetry() {
2144
- const settings = this._ctx?.settings;
2493
+ const settings = this.rawSettings();
2145
2494
  if (!settings) return {};
2146
2495
  const delaysMs = [
2147
2496
  150,
@@ -4709,6 +5058,7 @@ function createDeviceProxy(api, binding, opts) {
4709
5058
  getRetrainExportUrl: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getRetrainExportUrl", "query", input),
4710
5059
  setRetrainFrameComplete: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "setRetrainFrameComplete", "mutation", input),
4711
5060
  writeRetrainArchive: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "writeRetrainArchive", "mutation", input),
5061
+ offerRetrainArchive: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "offerRetrainArchive", "mutation", input),
4712
5062
  previewRetrainDataset: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "previewRetrainDataset", "query", input),
4713
5063
  createRetrainDatasetVersion: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "createRetrainDatasetVersion", "mutation", input),
4714
5064
  listRetrainDatasetVersions: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listRetrainDatasetVersions", "query", input),