@camstack/addon-export-alexa 1.2.138 → 1.2.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import * as crypto$1 from "node:crypto";
2
2
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
3
3
  import sharp from "sharp";
4
- //#region ../types/dist/event-category-B_0XEcLF.mjs
4
+ //#region ../types/dist/event-category-DaIZi00R.mjs
5
5
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
6
6
  EventCategory["SystemBoot"] = "system.boot";
7
7
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -80,6 +80,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
80
80
  */
81
81
  EventCategory["AddonLoadFailed"] = "addon.load-failed";
82
82
  /**
83
+ * A keychain value was stored or removed (D787). Carries the ref, its
84
+ * owner and the new version — NEVER the value. A process holding the ref
85
+ * drops its cached copy and the owning `BaseAddon` re-resolves its config.
86
+ * Telemetry (D8): a process that misses it still revalidates by version.
87
+ */
88
+ EventCategory["KeychainChanged"] = "keychain.changed";
89
+ /**
83
90
  * Addon recovered from a previous failure. Emitted when an addon
84
91
  * transitions from `failed` back to `healthy` (typically via the
85
92
  * monitor's auto-retry loop, or after manual `addons.retryLoad`).
@@ -5372,7 +5379,7 @@ var ZodIssueCode = {
5372
5379
  var ZodFirstPartyTypeKind;
5373
5380
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5374
5381
  //#endregion
5375
- //#region ../types/dist/sleep-B0JG7-_F.mjs
5382
+ //#region ../types/dist/sleep-Cr5YOk3m.mjs
5376
5383
  /**
5377
5384
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5378
5385
  * window to float samples (D455).
@@ -5451,6 +5458,97 @@ function buildAlawTable() {
5451
5458
  }
5452
5459
  buildUlawTable();
5453
5460
  buildAlawTable();
5461
+ /** Is this field's stored value a credential? */
5462
+ function isSecretConfigField(field) {
5463
+ if (field.type === "password") return true;
5464
+ return "secret" in field && field.secret === true;
5465
+ }
5466
+ /**
5467
+ * Is this field's value stored in the KEYCHAIN (D787)? Every secret field is,
5468
+ * by its TYPE, unless it opts out with `storage: 'inline'` — never by its name.
5469
+ */
5470
+ function isKeychainStoredField(field) {
5471
+ if (!isSecretConfigField(field)) return false;
5472
+ if (field.type === "textarea" && field.isJson === true) return false;
5473
+ return !("storage" in field && field.storage === "inline");
5474
+ }
5475
+ /**
5476
+ * What a settings blob holds where a secret used to be.
5477
+ *
5478
+ * A keychain-stored field never holds its value. It holds a self-describing
5479
+ * MARKER, `{"$secret":"secret://…","v":1}` — an object, never a string, so no
5480
+ * code can mistake it for a credential and no form can send it to a camera as
5481
+ * a password. The empty state is `null` / absent, never `''`.
5482
+ *
5483
+ * The same shapes travel to and from the admin UI:
5484
+ *
5485
+ * - read: `hydrateSchema` hands a secret field a {@link SecretFieldView}
5486
+ * (`{ $secret, v, set: true }`) or `null` — the browser learns whether a
5487
+ * value is set, never the value.
5488
+ * - write: an untouched field submits the view back unchanged (KEEP); a typed
5489
+ * value is a plain non-empty string (CHANGE); the Clear button submits
5490
+ * {@link SECRET_CLEAR} (CLEAR). `''`, `null` and the legacy redaction tokens
5491
+ * are KEEP — an empty string is what a defaults-fallback pushes back, and
5492
+ * treating it as "clear" is how a save destroyed a stored credential before.
5493
+ *
5494
+ * Pure and browser-safe.
5495
+ */
5496
+ var SECRET_CLEAR = { $secretClear: true };
5497
+ /** The `'***'` token D634 redaction writes for non-admins. */
5498
+ var LEGACY_DEVICE_REDACTION = "***";
5499
+ function isRecord$1(value) {
5500
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5501
+ }
5502
+ function makeSecretMarker(ref) {
5503
+ return {
5504
+ $secret: ref,
5505
+ v: 1
5506
+ };
5507
+ }
5508
+ /** A stored marker: an object whose `$secret` is a non-empty string. */
5509
+ function isSecretMarker(value) {
5510
+ if (!isRecord$1(value)) return false;
5511
+ const ref = value["$secret"];
5512
+ return typeof ref === "string" && ref.length > 0;
5513
+ }
5514
+ function isSecretClear(value) {
5515
+ return isRecord$1(value) && value["$secretClear"] === true;
5516
+ }
5517
+ /**
5518
+ * The UI view of a stored secret field value: a marker becomes "set", inline
5519
+ * plaintext becomes "set (inline)", nothing becomes `null`. A value never
5520
+ * survives this function.
5521
+ */
5522
+ function toSecretFieldView(stored) {
5523
+ if (isSecretMarker(stored)) return {
5524
+ $secret: stored.$secret,
5525
+ v: 1,
5526
+ set: true
5527
+ };
5528
+ if (typeof stored === "string" && stored.length > 0) return {
5529
+ $secret: null,
5530
+ v: 1,
5531
+ set: true,
5532
+ inline: true
5533
+ };
5534
+ return null;
5535
+ }
5536
+ /**
5537
+ * Classify an incoming value for a secret path. Only a non-empty string that is
5538
+ * not a redaction token sets a value; only {@link SECRET_CLEAR} clears.
5539
+ * Everything else — a marker (ours or anyone's), a UI view, `''`, `null`,
5540
+ * `undefined`, a redaction token, a non-string — keeps what is stored.
5541
+ */
5542
+ function classifySecretWrite(incoming) {
5543
+ if (isSecretClear(incoming)) return { kind: "clear" };
5544
+ if (typeof incoming !== "string") return { kind: "keep" };
5545
+ if (incoming.length === 0) return { kind: "keep" };
5546
+ if (incoming === "__camstack_redacted__" || incoming === LEGACY_DEVICE_REDACTION) return { kind: "keep" };
5547
+ return {
5548
+ kind: "set",
5549
+ value: incoming
5550
+ };
5551
+ }
5454
5552
  Object.fromEntries([
5455
5553
  {
5456
5554
  id: "overview",
@@ -5622,33 +5720,19 @@ Object.fromEntries([
5622
5720
  function isValuelessField(field) {
5623
5721
  return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button";
5624
5722
  }
5625
- /**
5626
- * Merge a `ConfigUISchema` with a raw `values` record into a
5627
- * `ConfigUISchemaWithValues`. Used by every `get*Settings` backend endpoint
5628
- * before returning to the admin UI. Groups are walked recursively.
5629
- *
5630
- * For each leaf field:
5631
- * - structural fields (`separator`, `info`, `button`) pass through unchanged
5632
- * - other fields get `value = values[key] ?? field.default ?? null`
5633
- *
5634
- * Unknown keys in `values` (keys not declared in the schema) are silently
5635
- * ignored — they don't contribute to the output. Missing keys fall back to
5636
- * the schema default or `null`. This mirrors the old backend behaviour where
5637
- * `FormBuilder` rendered `values[key] ?? field.default`.
5638
- */
5639
- function hydrateSchema(schema, values) {
5723
+ function hydrateSchema(schema, values, options = {}) {
5640
5724
  return {
5641
5725
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
5642
5726
  sections: schema.sections.map((section) => ({
5643
5727
  ...section,
5644
- fields: section.fields.map((field) => hydrateField(field, values))
5728
+ fields: section.fields.map((field) => hydrateField(field, values, options))
5645
5729
  }))
5646
5730
  };
5647
5731
  }
5648
- function hydrateField(field, values) {
5732
+ function hydrateField(field, values, options) {
5649
5733
  if (isValuelessField(field)) return field;
5650
5734
  if (field.type === "group") {
5651
- const hydratedChildren = field.fields.map((child) => hydrateField(child, values));
5735
+ const hydratedChildren = field.fields.map((child) => hydrateField(child, values, options));
5652
5736
  return {
5653
5737
  ...field,
5654
5738
  fields: hydratedChildren
@@ -5657,7 +5741,7 @@ function hydrateField(field, values) {
5657
5741
  if (field.type === "sub-tabs") {
5658
5742
  const hydratedTabs = field.tabs.map((tab) => ({
5659
5743
  ...tab,
5660
- fields: tab.fields.map((child) => hydrateField(child, values))
5744
+ fields: tab.fields.map((child) => hydrateField(child, values, options))
5661
5745
  }));
5662
5746
  return {
5663
5747
  ...field,
@@ -5667,6 +5751,33 @@ function hydrateField(field, values) {
5667
5751
  const key = field.key;
5668
5752
  const storedValue = Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
5669
5753
  const defaultValue = field.default;
5754
+ const asView = (v) => isSecretMarker(v) || options.inlineSecretsAsViews === true && v !== void 0;
5755
+ if (isKeychainStoredField(field) && asView(storedValue)) return {
5756
+ ...field,
5757
+ value: toSecretFieldView(storedValue)
5758
+ };
5759
+ if (isKeychainStoredField(field) && options.inlineSecretsAsViews === true) return {
5760
+ ...field,
5761
+ value: null
5762
+ };
5763
+ if (field.type === "editable-array" && Array.isArray(storedValue)) {
5764
+ const secretItemKeys = field.itemFields.filter((item) => "key" in item && isKeychainStoredField(item)).map((item) => "key" in item ? item.key : "");
5765
+ if (secretItemKeys.length > 0) {
5766
+ const rows = storedValue.map((row) => {
5767
+ if (row === null || typeof row !== "object" || Array.isArray(row)) return row;
5768
+ const out = { ...row };
5769
+ for (const k of secretItemKeys) {
5770
+ const v = Reflect.get(row, k);
5771
+ if (asView(v)) out[k] = toSecretFieldView(v);
5772
+ }
5773
+ return out;
5774
+ });
5775
+ return {
5776
+ ...field,
5777
+ value: rows
5778
+ };
5779
+ }
5780
+ }
5670
5781
  if (field.multiple) {
5671
5782
  const stored = Array.isArray(storedValue) ? storedValue : storedValue !== void 0 && storedValue !== null ? [storedValue] : [];
5672
5783
  const itemFallback = field.multiple.itemDefault !== void 0 ? field.multiple.itemDefault : defaultValue !== void 0 ? defaultValue : typeOf(field) === "string" ? "" : null;
@@ -5909,6 +6020,431 @@ function projectStore(store, perNodeKeys, nodeId) {
5909
6020
  }
5910
6021
  return out;
5911
6022
  }
6023
+ var GLOBAL_SCOPE = { kind: "global" };
6024
+ /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6025
+ function globalSecretScope() {
6026
+ return GLOBAL_SCOPE;
6027
+ }
6028
+ function encodeSegment(value) {
6029
+ return encodeURIComponent(value);
6030
+ }
6031
+ /**
6032
+ * The field path of a value inside an array item, addressed by the item's id:
6033
+ * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
6034
+ * `providers[authentik].clientSecret`.
6035
+ */
6036
+ function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6037
+ return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6038
+ }
6039
+ /** A write to a secret path could not be stored in the keychain, so it was refused. */
6040
+ var KeychainWriteRefusedError = class extends Error {
6041
+ fieldPath;
6042
+ constructor(fieldPath, cause) {
6043
+ super(`secret "${fieldPath}" was not saved: the keychain could not store it (${cause instanceof Error ? cause.message : String(cause)}). Nothing was written.`);
6044
+ this.fieldPath = fieldPath;
6045
+ this.name = "KeychainWriteRefusedError";
6046
+ }
6047
+ };
6048
+ var KEEP = { kind: "keep" };
6049
+ function isRecord(value) {
6050
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6051
+ }
6052
+ function defaultMintId() {
6053
+ return globalThis.crypto.randomUUID();
6054
+ }
6055
+ /** Every marker ref anywhere under `value`. */
6056
+ function collectSecretMarkerRefs(value, out = []) {
6057
+ if (isSecretMarker(value)) {
6058
+ out.push(value.$secret);
6059
+ return out;
6060
+ }
6061
+ if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6062
+ else if (isRecord(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6063
+ return out;
6064
+ }
6065
+ async function storedValueEquals(secrets, marker, candidate) {
6066
+ const resolved = (await secrets.resolve([marker.$secret]))[marker.$secret];
6067
+ return resolved?.kind === "value" && resolved.value === candidate;
6068
+ }
6069
+ async function writeLeaf(ctx, incoming, stored, fieldPath) {
6070
+ const intent = classifySecretWrite(incoming);
6071
+ const ref = ctx.secrets.ref(ctx.scope, fieldPath);
6072
+ const storedIsOurs = isSecretMarker(stored) && stored.$secret === ref;
6073
+ if (intent.kind === "keep") return KEEP;
6074
+ if (intent.kind === "set" && !ctx.operatorWrite && typeof stored === "string" && stored === intent.value) return KEEP;
6075
+ if (intent.kind === "clear") {
6076
+ if (storedIsOurs) ctx.deletes.push(ref);
6077
+ return {
6078
+ kind: "value",
6079
+ value: null
6080
+ };
6081
+ }
6082
+ if (storedIsOurs && await storedValueEquals(ctx.secrets, stored, intent.value)) return {
6083
+ kind: "value",
6084
+ value: stored
6085
+ };
6086
+ try {
6087
+ await ctx.secrets.put(ref, intent.value);
6088
+ } catch (err) {
6089
+ throw new KeychainWriteRefusedError(fieldPath, err);
6090
+ }
6091
+ return {
6092
+ kind: "value",
6093
+ value: makeSecretMarker(ref)
6094
+ };
6095
+ }
6096
+ /** Apply a member result onto `out`: KEEP restores the stored member, or removes it. */
6097
+ function applyMember(out, key, result, storedParent) {
6098
+ if (result.kind === "value") {
6099
+ out[key] = result.value;
6100
+ return;
6101
+ }
6102
+ if (Object.prototype.hasOwnProperty.call(storedParent, key)) out[key] = storedParent[key];
6103
+ else delete out[key];
6104
+ }
6105
+ async function writeMembers(ctx, children, incoming, stored, pathOf) {
6106
+ const out = { ...incoming };
6107
+ for (const [member, node] of children) applyMember(out, member, await writeNode(ctx, node, incoming[member], stored[member], pathOf(member)), stored);
6108
+ return out;
6109
+ }
6110
+ function rowId(row, idKey) {
6111
+ const raw = row[idKey];
6112
+ if (typeof raw === "string" && raw.length > 0) return raw;
6113
+ if (typeof raw === "number" && Number.isFinite(raw)) return String(raw);
6114
+ return null;
6115
+ }
6116
+ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6117
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord) : [];
6118
+ if (!Array.isArray(incoming)) {
6119
+ ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6120
+ return {
6121
+ kind: "value",
6122
+ value: incoming
6123
+ };
6124
+ }
6125
+ const storedById = /* @__PURE__ */ new Map();
6126
+ for (const row of storedRows) {
6127
+ const id = rowId(row, node.idKey);
6128
+ if (id !== null) storedById.set(id, row);
6129
+ }
6130
+ const kept = /* @__PURE__ */ new Set();
6131
+ const rows = [];
6132
+ for (const row of incoming) {
6133
+ if (!isRecord(row)) {
6134
+ rows.push(row);
6135
+ continue;
6136
+ }
6137
+ const id = rowId(row, node.idKey) ?? ctx.mintId();
6138
+ kept.add(id);
6139
+ const withId = {
6140
+ ...row,
6141
+ [node.idKey]: row[node.idKey] ?? id
6142
+ };
6143
+ if (rowId(withId, node.idKey) === null) withId[node.idKey] = id;
6144
+ const storedRow = storedById.get(id) ?? {};
6145
+ rows.push(await writeMembers(ctx, node.item, withId, storedRow, (member) => arrayItemFieldPath(fieldPath, id, member)));
6146
+ }
6147
+ for (const [id, row] of storedById) if (!kept.has(id)) ctx.deletes.push(...collectSecretMarkerRefs(row));
6148
+ return {
6149
+ kind: "value",
6150
+ value: rows
6151
+ };
6152
+ }
6153
+ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6154
+ switch (node.kind) {
6155
+ case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
6156
+ case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
6157
+ case "object": {
6158
+ if (!isRecord(incoming)) {
6159
+ if (incoming === void 0) return KEEP;
6160
+ ctx.deletes.push(...collectSecretMarkerRefs(stored));
6161
+ return {
6162
+ kind: "value",
6163
+ value: incoming
6164
+ };
6165
+ }
6166
+ const storedObj = isRecord(stored) ? stored : {};
6167
+ return {
6168
+ kind: "value",
6169
+ value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
6170
+ };
6171
+ }
6172
+ }
6173
+ }
6174
+ /** Split a perNode-scoped key (`key@nodeId`). */
6175
+ function splitNodeKey(key) {
6176
+ const at = key.indexOf("@");
6177
+ return at <= 0 ? {
6178
+ base: key,
6179
+ nodeId: null
6180
+ } : {
6181
+ base: key.slice(0, at),
6182
+ nodeId: key.slice(at + 1)
6183
+ };
6184
+ }
6185
+ /** Rewrite a settings patch so no secret value reaches the blob. See the file doc. */
6186
+ async function transformSecretWrite(input) {
6187
+ const deletes = [];
6188
+ const base = {
6189
+ secrets: input.secrets,
6190
+ mintId: input.mintId ?? defaultMintId,
6191
+ deletes,
6192
+ operatorWrite: input.operatorWrite === true
6193
+ };
6194
+ const out = {};
6195
+ for (const [key, incoming] of Object.entries(input.patch)) {
6196
+ const { base: baseKey, nodeId } = splitNodeKey(key);
6197
+ const node = input.tree.get(baseKey);
6198
+ if (node === void 0) {
6199
+ out[key] = incoming;
6200
+ continue;
6201
+ }
6202
+ const scope = nodeId !== null && nodeId.length > 0 ? {
6203
+ kind: "node",
6204
+ nodeId
6205
+ } : input.scope ?? globalSecretScope();
6206
+ const result = await writeNode({
6207
+ ...base,
6208
+ scope
6209
+ }, node, incoming, input.stored[key], baseKey);
6210
+ if (result.kind === "value") out[key] = result.value;
6211
+ }
6212
+ return {
6213
+ patch: out,
6214
+ afterWrite: async () => {
6215
+ if (deletes.length === 0) return { orphaned: [] };
6216
+ try {
6217
+ await input.secrets.delete([...new Set(deletes)]);
6218
+ return { orphaned: [] };
6219
+ } catch {
6220
+ return { orphaned: [...new Set(deletes)] };
6221
+ }
6222
+ }
6223
+ };
6224
+ }
6225
+ /**
6226
+ * Replace every marker in `value` with its keychain value. Returns the SAME
6227
+ * reference when there is no marker (no allocation, no keychain call).
6228
+ */
6229
+ async function resolveSecretMarkers(value, secrets) {
6230
+ const refs = collectSecretMarkerRefs(value);
6231
+ if (refs.length === 0) return {
6232
+ value,
6233
+ unresolved: []
6234
+ };
6235
+ const answers = await secrets.resolve(refs);
6236
+ const unresolved = [];
6237
+ for (const ref of new Set(refs)) {
6238
+ const answer = answers[ref];
6239
+ if (answer === void 0) unresolved.push({
6240
+ ref,
6241
+ reason: "not-initialized"
6242
+ });
6243
+ else if (answer.kind === "unavailable") unresolved.push({
6244
+ ref,
6245
+ reason: answer.reason
6246
+ });
6247
+ else if (answer.kind === "missing" || answer.kind === "forbidden") unresolved.push({
6248
+ ref,
6249
+ reason: answer.kind
6250
+ });
6251
+ }
6252
+ const substitute = (v) => {
6253
+ if (isSecretMarker(v)) {
6254
+ const answer = answers[v.$secret];
6255
+ return answer?.kind === "value" ? {
6256
+ keep: true,
6257
+ value: answer.value
6258
+ } : {
6259
+ keep: false,
6260
+ value: void 0
6261
+ };
6262
+ }
6263
+ if (Array.isArray(v)) {
6264
+ const items = [];
6265
+ for (const item of v) {
6266
+ const s = substitute(item);
6267
+ if (s.keep) items.push(s.value);
6268
+ }
6269
+ return {
6270
+ keep: true,
6271
+ value: items
6272
+ };
6273
+ }
6274
+ if (isRecord(v)) {
6275
+ const out = {};
6276
+ for (const [k, child] of Object.entries(v)) {
6277
+ const s = substitute(child);
6278
+ if (s.keep) out[k] = s.value;
6279
+ }
6280
+ return {
6281
+ keep: true,
6282
+ value: out
6283
+ };
6284
+ }
6285
+ return {
6286
+ keep: true,
6287
+ value: v
6288
+ };
6289
+ };
6290
+ return {
6291
+ value: substitute(value).value,
6292
+ unresolved
6293
+ };
6294
+ }
6295
+ function touchesTree(tree, patch) {
6296
+ if (tree.size === 0) return false;
6297
+ for (const key of Object.keys(patch)) {
6298
+ const at = key.indexOf("@");
6299
+ if (tree.has(at > 0 ? key.slice(0, at) : key)) return true;
6300
+ }
6301
+ return false;
6302
+ }
6303
+ /** The blob as stored now; a failed read REFUSES the write instead of guessing. */
6304
+ async function readStoredAddonBlob(raw) {
6305
+ const threeWay = raw.readAddonStoreResult;
6306
+ if (threeWay === void 0) return raw.readAddonStore();
6307
+ const result = await threeWay.call(raw);
6308
+ if (result.kind === "failed") throw new Error(`settings write refused: the stored blob could not be read (${result.error})`);
6309
+ return result.kind === "missing" ? {} : result.value;
6310
+ }
6311
+ function createSecretAwareSettings(raw, opts) {
6312
+ const chains = /* @__PURE__ */ new Map();
6313
+ const serialize = (key, fn) => {
6314
+ const next = (chains.get(key) ?? Promise.resolve()).then(fn, fn);
6315
+ const tail = next.catch(() => void 0);
6316
+ chains.set(key, tail);
6317
+ tail.then(() => {
6318
+ if (chains.get(key) === tail) chains.delete(key);
6319
+ });
6320
+ return next;
6321
+ };
6322
+ const write = async (chainKey, tree, scope, patch, readStored, rawWrite) => {
6323
+ if (!touchesTree(tree, patch)) return rawWrite(patch);
6324
+ return serialize(chainKey, async () => {
6325
+ const stored = await readStored();
6326
+ const result = await transformSecretWrite({
6327
+ tree,
6328
+ secrets: opts.secrets,
6329
+ patch,
6330
+ stored,
6331
+ ...scope !== void 0 ? { scope } : {},
6332
+ operatorWrite: opts.isOperatorWrite?.() === true
6333
+ });
6334
+ if (Object.keys(result.patch).length > 0) await rawWrite(result.patch);
6335
+ const { orphaned } = await result.afterWrite();
6336
+ if (orphaned.length > 0) opts.onOrphaned(orphaned);
6337
+ });
6338
+ };
6339
+ const wrapped = {
6340
+ writeAddonStore: (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p)),
6341
+ writeDeviceStore: (deviceId, patch) => write(`device:${deviceId}`, opts.deviceTree(), {
6342
+ kind: "device",
6343
+ deviceId
6344
+ }, patch, () => raw.readDeviceStore(deviceId), (p) => raw.writeDeviceStore(deviceId, p))
6345
+ };
6346
+ return new Proxy(raw, { get(target, prop, receiver) {
6347
+ if (prop === "writeAddonStore") return wrapped.writeAddonStore;
6348
+ if (prop === "writeDeviceStore") return wrapped.writeDeviceStore;
6349
+ const value = Reflect.get(target, prop, receiver);
6350
+ return typeof value === "function" ? value.bind(target) : value;
6351
+ } });
6352
+ }
6353
+ /** The context the addon sees: identical, except that its settings writes are secret-aware. */
6354
+ function withSecretAwareSettings(context, opts) {
6355
+ const raw = context.settings;
6356
+ if (raw === void 0) return context;
6357
+ const settings = createSecretAwareSettings(raw, opts);
6358
+ return new Proxy(context, { get(target, prop, receiver) {
6359
+ if (prop === "settings") return settings;
6360
+ return Reflect.get(target, prop, receiver);
6361
+ } });
6362
+ }
6363
+ var LEAF = { kind: "leaf" };
6364
+ var DEFAULT_ITEM_ID_KEY = "id";
6365
+ function collectFields(fields, out) {
6366
+ for (const field of fields) {
6367
+ if (field.type === "group") {
6368
+ collectFields(field.fields, out);
6369
+ continue;
6370
+ }
6371
+ if (field.type === "sub-tabs") {
6372
+ for (const tab of field.tabs) collectFields(tab.fields, out);
6373
+ continue;
6374
+ }
6375
+ if (field.type === "editable-array") {
6376
+ const item = /* @__PURE__ */ new Map();
6377
+ for (const itemField of field.itemFields) if ("key" in itemField && isKeychainStoredField(itemField)) item.set(itemField.key, LEAF);
6378
+ if (item.size > 0) out.set(field.key, {
6379
+ kind: "array",
6380
+ idKey: field.itemIdKey ?? DEFAULT_ITEM_ID_KEY,
6381
+ item
6382
+ });
6383
+ continue;
6384
+ }
6385
+ if (!("key" in field) || typeof field.key !== "string") continue;
6386
+ if (isKeychainStoredField(field)) out.set(field.key, LEAF);
6387
+ }
6388
+ }
6389
+ /** The secret tree a settings schema declares. */
6390
+ function secretTreeFromSchema(schema) {
6391
+ const out = /* @__PURE__ */ new Map();
6392
+ if (schema === null) return out;
6393
+ for (const section of schema.sections) collectFields(section.fields, out);
6394
+ return out;
6395
+ }
6396
+ /** Parse `a.b[id].c` into segments; throws on a malformed path (a declaration bug). */
6397
+ function parseSecretStatePath(path) {
6398
+ const segments = [];
6399
+ for (const raw of path.split(".")) {
6400
+ const m = /^([^[\]]+)(?:\[([^[\]]+)\])?$/.exec(raw);
6401
+ if (m === null || m[1] === void 0) throw new Error(`secretStatePaths: malformed path "${path}"`);
6402
+ segments.push(m[2] !== void 0 ? {
6403
+ key: m[1],
6404
+ idKey: m[2]
6405
+ } : { key: m[1] });
6406
+ }
6407
+ if (segments.length === 0) throw new Error(`secretStatePaths: empty path`);
6408
+ if (segments[segments.length - 1]?.idKey !== void 0) throw new Error(`secretStatePaths: "${path}" ends on an array — name the member that is secret`);
6409
+ return segments;
6410
+ }
6411
+ function insert(into, segments) {
6412
+ const [head, ...rest] = segments;
6413
+ if (head === void 0) return;
6414
+ if (rest.length === 0) {
6415
+ into.set(head.key, LEAF);
6416
+ return;
6417
+ }
6418
+ const existing = into.get(head.key);
6419
+ if (head.idKey !== void 0) {
6420
+ const item = new Map(existing?.kind === "array" ? existing.item : []);
6421
+ insert(item, rest);
6422
+ into.set(head.key, {
6423
+ kind: "array",
6424
+ idKey: head.idKey,
6425
+ item
6426
+ });
6427
+ return;
6428
+ }
6429
+ const children = new Map(existing?.kind === "object" ? existing.children : []);
6430
+ insert(children, rest);
6431
+ into.set(head.key, {
6432
+ kind: "object",
6433
+ children
6434
+ });
6435
+ }
6436
+ /** Add hidden-state paths (`secretStatePaths`) to a schema-derived tree. */
6437
+ function mergeSecretStatePaths(tree, paths) {
6438
+ if (paths.length === 0) return tree;
6439
+ const out = new Map(tree);
6440
+ for (const path of paths) insert(out, parseSecretStatePath(path));
6441
+ return out;
6442
+ }
6443
+ /** First retry of a secret the keychain could not serve; doubles to {@link SECRET_RETRY_MAX_MS}. */
6444
+ var SECRET_RETRY_FIRST_MS = 5e3;
6445
+ var SECRET_RETRY_MAX_MS = 5 * 6e4;
6446
+ /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
6447
+ var SECRET_CHANGE_DEBOUNCE_MS = 250;
5912
6448
  /**
5913
6449
  * Base class for CamStack addons. Eliminates settings boilerplate:
5914
6450
  *
@@ -5978,6 +6514,25 @@ var BaseAddon = class {
5978
6514
  settingsStoreReady = false;
5979
6515
  /** Default config values. Provided via constructor. */
5980
6516
  defaults;
6517
+ /**
6518
+ * Hidden-state paths that hold secrets (D787) — values the addon writes
6519
+ * itself (`updateGlobalSettings`, `state()`) that no form field declares.
6520
+ * Stored in the keychain exactly like a `password` field. Syntax:
6521
+ * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
6522
+ * (`key[<id field>]` addresses array items by id, never by index).
6523
+ */
6524
+ secretStatePaths = [];
6525
+ _addonSecretTree = null;
6526
+ _deviceSecretTree = null;
6527
+ /** Refs the last resolved config pointed at — a change to one re-resolves. */
6528
+ _configSecretRefs = /* @__PURE__ */ new Set();
6529
+ _unresolvedSecrets = [];
6530
+ _secretRetryTimer = null;
6531
+ _secretRetryDelayMs = SECRET_RETRY_FIRST_MS;
6532
+ _secretChangeTimer = null;
6533
+ _secretChangeUnsub = null;
6534
+ /** >0 while an OPERATOR's settings write is being applied. */
6535
+ _operatorWriteDepth = 0;
5981
6536
  constructor(defaults) {
5982
6537
  this.defaults = defaults;
5983
6538
  this._config = { ...defaults };
@@ -6013,8 +6568,18 @@ var BaseAddon = class {
6013
6568
  return this._config;
6014
6569
  }
6015
6570
  async initialize(context) {
6016
- this._ctx = context;
6571
+ const secrets = context.secrets;
6572
+ this._ctx = secrets !== void 0 ? withSecretAwareSettings(context, {
6573
+ secrets,
6574
+ addonTree: () => this.addonSecretTree(),
6575
+ deviceTree: () => this.deviceSecretTree(),
6576
+ onOrphaned: (refs) => context.logger.warn("keychain refs dropped by a settings write could not be deleted", { meta: { refs: [...refs] } }),
6577
+ isOperatorWrite: () => this._operatorWriteDepth > 0
6578
+ }) : context;
6017
6579
  await this.resolveConfig();
6580
+ if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
6581
+ if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
6582
+ });
6018
6583
  const result = await this.onInitialize();
6019
6584
  this.emitLifecycle(EventCategory.AddonStarted);
6020
6585
  const normalized = normalizeAddonInitResult(result);
@@ -6047,6 +6612,12 @@ var BaseAddon = class {
6047
6612
  */
6048
6613
  async onHubReachable() {}
6049
6614
  async shutdown() {
6615
+ this._secretChangeUnsub?.();
6616
+ this._secretChangeUnsub = null;
6617
+ if (this._secretRetryTimer !== null) clearTimeout(this._secretRetryTimer);
6618
+ if (this._secretChangeTimer !== null) clearTimeout(this._secretChangeTimer);
6619
+ this._secretRetryTimer = null;
6620
+ this._secretChangeTimer = null;
6050
6621
  this.emitLifecycle(EventCategory.AddonStopped);
6051
6622
  if (this.autoEmitReadiness) this.emitReadinessForProviders("down");
6052
6623
  await this.onShutdown();
@@ -6125,7 +6696,7 @@ var BaseAddon = class {
6125
6696
  return hydrateSchema(schema, overlay ? {
6126
6697
  ...projected,
6127
6698
  ...overlay
6128
- } : projected);
6699
+ } : projected, { inlineSecretsAsViews: true });
6129
6700
  }
6130
6701
  /**
6131
6702
  * The integration-level view of this addon's settings: exactly the sections
@@ -6174,7 +6745,7 @@ var BaseAddon = class {
6174
6745
  return hydrateSchema({
6175
6746
  ...schema,
6176
6747
  sections
6177
- }, projected);
6748
+ }, projected, { inlineSecretsAsViews: true });
6178
6749
  }
6179
6750
  /**
6180
6751
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -6228,18 +6799,50 @@ var BaseAddon = class {
6228
6799
  value: result.value[key]
6229
6800
  };
6230
6801
  }
6802
+ /**
6803
+ * The OPERATOR's write — a settings form or an admin API call, dispatched by
6804
+ * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
6805
+ * the operator sends is stored as sent even when it equals a pre-keychain
6806
+ * inline value: the operator re-entering a password is a change, an addon
6807
+ * writing back what it read is not (D787).
6808
+ */
6809
+ async updateGlobalSettingsAsOperator(patch, nodeId) {
6810
+ this._operatorWriteDepth++;
6811
+ try {
6812
+ await this.applyGlobalPatch(patch, nodeId);
6813
+ } finally {
6814
+ this._operatorWriteDepth--;
6815
+ }
6816
+ }
6231
6817
  async updateGlobalSettings(patch, nodeId) {
6818
+ await this.applyGlobalPatch(patch, nodeId);
6819
+ }
6820
+ /**
6821
+ * REMOVE stored secrets — their keychain values and their markers (D787).
6822
+ *
6823
+ * Writing `''` to a secret field KEEPS what is stored (an empty string is
6824
+ * what a defaults fallback writes back, and it must never destroy a
6825
+ * credential), so code that means "forget this token" says so here. Without
6826
+ * a keychain (an older kernel) it writes `''`, as such code always did.
6827
+ */
6828
+ async clearSecrets(keys, nodeId) {
6829
+ const tree = this.addonSecretTree();
6830
+ const keychain = this._ctx?.secrets !== void 0;
6831
+ const patch = {};
6832
+ for (const key of keys) patch[key] = keychain && tree.has(key) ? SECRET_CLEAR : "";
6833
+ await this.applyGlobalPatch(patch, nodeId);
6834
+ }
6835
+ async applyGlobalPatch(barePatch, nodeId) {
6232
6836
  const keys = this.perNodeKeys();
6233
6837
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
6234
6838
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
6235
- const barePatch = patch;
6236
6839
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
6237
6840
  await this._ctx?.settings?.writeAddonStore(scoped);
6238
6841
  if (target !== localNode) return;
6239
6842
  await this.resolveConfig();
6240
6843
  await this.onConfigChanged();
6241
6844
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
6242
- this.maybeAutoRestart(patch, this.globalSettingsSchema());
6845
+ this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
6243
6846
  }
6244
6847
  /**
6245
6848
  * The set of field keys the global settings schema declares `perNode: true`
@@ -6264,7 +6867,7 @@ var BaseAddon = class {
6264
6867
  * time to return its response before the addon is torn down and
6265
6868
  * re-initialised by `AddonRegistryService.restartAddon`.
6266
6869
  */
6267
- maybeAutoRestart(patch, schema) {
6870
+ maybeAutoRestart(patchKeys, schema) {
6268
6871
  if (!schema) return;
6269
6872
  const restartKeys = /* @__PURE__ */ new Set();
6270
6873
  for (const section of schema.sections) for (const field of section.fields) {
@@ -6272,13 +6875,13 @@ var BaseAddon = class {
6272
6875
  if (field.requiresRestart) restartKeys.add(field.key);
6273
6876
  }
6274
6877
  if (restartKeys.size === 0) return;
6275
- if (!Object.keys(patch).some((k) => restartKeys.has(k))) return;
6878
+ if (!patchKeys.some((k) => restartKeys.has(k))) return;
6276
6879
  const ctx = this._ctx;
6277
6880
  if (!ctx) return;
6278
6881
  const addonId = ctx.id;
6279
6882
  setImmediate(() => {
6280
6883
  ctx.api.addons?.restartAddon?.mutate({ addonId }).then(() => {
6281
- ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: Object.keys(patch).filter((k) => restartKeys.has(k)) } });
6884
+ ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: patchKeys.filter((k) => restartKeys.has(k)) } });
6282
6885
  }).catch((err) => {
6283
6886
  ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6284
6887
  });
@@ -6287,7 +6890,7 @@ var BaseAddon = class {
6287
6890
  async getDeviceSettings(deviceId) {
6288
6891
  const schema = this.deviceSettingsSchema();
6289
6892
  if (!schema) return { sections: [] };
6290
- return hydrateSchema(schema, await this._ctx?.settings?.readDeviceStore(deviceId) ?? {});
6893
+ return hydrateSchema(schema, await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, { inlineSecretsAsViews: true });
6291
6894
  }
6292
6895
  async updateDeviceSettings(deviceId, patch) {
6293
6896
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
@@ -6417,7 +7020,7 @@ var BaseAddon = class {
6417
7020
  * selection at boot (absent scoped key ⇒ the constructor default wins).
6418
7021
  */
6419
7022
  async resolveConfig() {
6420
- const stored = await this.readAddonStoreWithRetry();
7023
+ const stored = await this.resolveStoredSecrets(await this.readAddonStoreWithRetry());
6421
7024
  const perNode = this.perNodeKeys();
6422
7025
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
6423
7026
  const resolved = { ...this.defaults };
@@ -6442,7 +7045,7 @@ var BaseAddon = class {
6442
7045
  key,
6443
7046
  schema,
6444
7047
  fallback,
6445
- read: () => this.readAddonStoreWithRetry(),
7048
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
6446
7049
  write: async (patch) => {
6447
7050
  await this._ctx?.settings?.writeAddonStore(patch);
6448
7051
  },
@@ -6459,7 +7062,7 @@ var BaseAddon = class {
6459
7062
  key,
6460
7063
  schema,
6461
7064
  fallback,
6462
- read: async () => await this._ctx?.settings?.readDeviceStore(deviceId) ?? {},
7065
+ read: async () => this.resolveStoredSecrets(await this._ctx?.settings?.readDeviceStore(deviceId) ?? {}, false),
6463
7066
  write: async (patch) => {
6464
7067
  await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
6465
7068
  },
@@ -6471,6 +7074,70 @@ var BaseAddon = class {
6471
7074
  } })
6472
7075
  });
6473
7076
  }
7077
+ /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
7078
+ addonSecretTree() {
7079
+ this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
7080
+ return this._addonSecretTree;
7081
+ }
7082
+ deviceSecretTree() {
7083
+ this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
7084
+ return this._deviceSecretTree;
7085
+ }
7086
+ /**
7087
+ * Secrets of this addon's stored config that could not be resolved on the
7088
+ * last read, with why. Non-empty means the addon is running WITHOUT them —
7089
+ * an addon whose function needs one should report itself degraded rather
7090
+ * than start on an empty credential.
7091
+ */
7092
+ get unresolvedSecrets() {
7093
+ return this._unresolvedSecrets;
7094
+ }
7095
+ /**
7096
+ * Substitute every keychain marker in a stored blob with its value. With no
7097
+ * keychain (no `ctx.secrets`) or no marker, the blob comes back unchanged.
7098
+ * An unresolvable value is LEFT OUT (never `''`), logged by ref — never by
7099
+ * value — and, when the cause can pass, retried with backoff. Nothing is
7100
+ * ever written back from here.
7101
+ */
7102
+ async resolveStoredSecrets(blob, trackForConfig = true) {
7103
+ const secrets = this._ctx?.secrets;
7104
+ if (secrets === void 0) return blob;
7105
+ const refs = collectSecretMarkerRefs(blob);
7106
+ if (trackForConfig) this._configSecretRefs = new Set(refs);
7107
+ if (refs.length === 0) {
7108
+ if (trackForConfig) this._unresolvedSecrets = [];
7109
+ return blob;
7110
+ }
7111
+ const { value, unresolved } = await resolveSecretMarkers(blob, secrets);
7112
+ if (trackForConfig) {
7113
+ this._unresolvedSecrets = unresolved;
7114
+ if (unresolved.length > 0) {
7115
+ this._ctx?.logger?.error?.("secret unavailable — running without it, will retry", { meta: { secrets: unresolved.map((u) => ({
7116
+ ref: u.ref,
7117
+ reason: u.reason
7118
+ })) } });
7119
+ if (unresolved.some((u) => u.reason !== "missing" && u.reason !== "forbidden")) {
7120
+ this.scheduleSecretReresolve(this._secretRetryDelayMs);
7121
+ this._secretRetryDelayMs = Math.min(this._secretRetryDelayMs * 2, SECRET_RETRY_MAX_MS);
7122
+ }
7123
+ } else this._secretRetryDelayMs = SECRET_RETRY_FIRST_MS;
7124
+ }
7125
+ return value;
7126
+ }
7127
+ /** Re-resolve the config (a keychain-only change, or a retry) and apply it. */
7128
+ scheduleSecretReresolve(delayMs) {
7129
+ const timerOf = delayMs === SECRET_CHANGE_DEBOUNCE_MS ? "_secretChangeTimer" : "_secretRetryTimer";
7130
+ if (this[timerOf] !== null) return;
7131
+ const timer = setTimeout(() => {
7132
+ this[timerOf] = null;
7133
+ if (this._ctx === null) return;
7134
+ this.resolveConfig().then(() => this.onConfigChanged()).catch((err) => {
7135
+ this._ctx?.logger?.error?.("re-resolving secrets failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
7136
+ });
7137
+ }, delayMs);
7138
+ timer.unref?.();
7139
+ this[timerOf] = timer;
7140
+ }
6474
7141
  /**
6475
7142
  * Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
6476
7143
  * transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
@@ -14275,6 +14942,196 @@ id: union([string(), number()]) }), {
14275
14942
  kind: "mutation",
14276
14943
  auth: "admin"
14277
14944
  });
14945
+ /**
14946
+ * `keychain` — the hub's encrypted store of secret values (D787).
14947
+ *
14948
+ * Settings blobs never hold a secret: they hold a marker
14949
+ * `{"$secret":"secret://<owner>/<scope>/<fieldPath>","v":1}` and the value
14950
+ * lives here, AES-256-GCM under a data key that a master key wraps. This cap
14951
+ * is the OWNER's door to its own values: every method is owner-checked
14952
+ * against the CALLER, never against anything the caller claims.
14953
+ *
14954
+ * Who the caller is: the transport stamps `callerAddonId` into the args of a
14955
+ * call that crossed a process (the parent `LocalChildRegistry` from the UDS
14956
+ * connection, the agent from its child's connection carried in
14957
+ * `$hub-cap-fwd`'s envelope). A stamp always overwrites; a wire call with no
14958
+ * stamp has its claim stripped. Only a hub in-process caller may state its own
14959
+ * identity — `ctx.secrets` does that with the addon id closed over. This is
14960
+ * identity against BUGS and misrouting, not against a malicious addon (every
14961
+ * runner is the same OS user): see the threat model in
14962
+ * `docs/design/2026-10-08-secrets-keychain.md`.
14963
+ *
14964
+ * `callerAddonId` is deliberately NOT in any input schema: the tRPC router
14965
+ * parses the input, so a session can never supply it, and the internal mount
14966
+ * floors every method to admin anyway (D240).
14967
+ *
14968
+ * Internal: addons reach it through `ctx.secrets`, never directly.
14969
+ */
14970
+ /** Why a value cannot be served right now. Never folded into "missing". */
14971
+ var KeychainUnavailableReasonSchema = _enum([
14972
+ "key-missing",
14973
+ "wrong-key",
14974
+ "key-file-unreadable",
14975
+ "store-unavailable",
14976
+ "not-initialized",
14977
+ "decrypt-failed"
14978
+ ]);
14979
+ var KeychainStateSchema = discriminatedUnion("state", [object({ state: literal("ready") }), object({
14980
+ state: literal("locked"),
14981
+ reason: KeychainUnavailableReasonSchema
14982
+ })]);
14983
+ var KeychainResolveEntrySchema = discriminatedUnion("kind", [
14984
+ object({
14985
+ kind: literal("value"),
14986
+ value: string(),
14987
+ version: number().int()
14988
+ }),
14989
+ object({ kind: literal("missing") }),
14990
+ object({
14991
+ kind: literal("unavailable"),
14992
+ reason: KeychainUnavailableReasonSchema
14993
+ }),
14994
+ object({ kind: literal("forbidden") })
14995
+ ]);
14996
+ var KeychainResolveResultSchema = record(string(), KeychainResolveEntrySchema);
14997
+ /** Metadata of one stored value — never the value. */
14998
+ var KeychainSecretMetaSchema = object({
14999
+ ref: string(),
15000
+ owner: string(),
15001
+ /** Formatted scope (`global`, `node/<id>`, `device/<id>`, …). */
15002
+ scope: string(),
15003
+ fieldPath: string(),
15004
+ version: number().int(),
15005
+ createdAt: number(),
15006
+ updatedAt: number(),
15007
+ /** `addon:<id>` | `user:<id>` | `system`. */
15008
+ updatedBy: string(),
15009
+ /** Coarse — written at most once an hour per ref. `null` = never read. */
15010
+ lastReadAt: number().nullable()
15011
+ });
15012
+ var RefListSchema = array(string().min(1)).max(512);
15013
+ method(object({
15014
+ ref: string().min(1),
15015
+ value: string().min(1)
15016
+ }), object({
15017
+ ref: string(),
15018
+ version: number().int()
15019
+ }), {
15020
+ kind: "mutation",
15021
+ auth: "admin"
15022
+ }), method(object({ refs: RefListSchema }), KeychainResolveResultSchema, { auth: "admin" }), method(object({ refs: RefListSchema }), object({ deleted: number().int() }), {
15023
+ kind: "mutation",
15024
+ auth: "admin"
15025
+ }), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
15026
+ var KeychainKeySourceSchema = object({
15027
+ kind: _enum([
15028
+ "config-file",
15029
+ "previous-config-file",
15030
+ "platform",
15031
+ "default-file"
15032
+ ]),
15033
+ /** File sources only — a path is not a secret. */
15034
+ path: string().optional(),
15035
+ /**
15036
+ * True when a whole-volume backup of the data directory carries this key
15037
+ * next to the database it protects (the default file). The page warns.
15038
+ */
15039
+ sharesBackupWithDatabase: boolean()
15040
+ });
15041
+ var KeychainStatusSchema = object({
15042
+ keychain: KeychainStateSchema,
15043
+ /** The source of the master key in use; `null` while locked. */
15044
+ keySource: KeychainKeySourceSchema.nullable(),
15045
+ /** The source configured to be primary — differs from `keySource` while a rotation is pending. */
15046
+ configuredSource: KeychainKeySourceSchema,
15047
+ /** Short fingerprint of the master key in use (`HMAC(KEK, label)`, 16 hex). */
15048
+ fingerprint: string().nullable(),
15049
+ activeKeyId: string().nullable(),
15050
+ /**
15051
+ * The keychain unlocked with a NON-primary source: the configured primary
15052
+ * key is not the one that wraps the data key yet. `rotateMasterKey` moves it.
15053
+ */
15054
+ rotationPending: boolean(),
15055
+ /** Wrapped copies of the data key under a previous master key, kept until purged. */
15056
+ retiredWraps: number().int(),
15057
+ recoveryKeyDownloadedAt: number().nullable(),
15058
+ secretCount: number().int()
15059
+ });
15060
+ /**
15061
+ * Whether a stored value is still pointed at by its owner's settings.
15062
+ * `unknown` is a first-class answer: a scope whose blob could not be read is
15063
+ * never reported as an orphan (and never deleted as one).
15064
+ */
15065
+ var KeychainSecretUsageSchema = _enum([
15066
+ "in-use",
15067
+ "orphan",
15068
+ "unknown"
15069
+ ]);
15070
+ var KeychainAdminSecretSchema = KeychainSecretMetaSchema.extend({ usage: KeychainSecretUsageSchema });
15071
+ var KeychainAuditActionSchema = _enum([
15072
+ "put",
15073
+ "delete",
15074
+ "reveal",
15075
+ "rotate-master-key",
15076
+ "purge-retired-keys",
15077
+ "download-recovery-key",
15078
+ "delete-orphans",
15079
+ "reset",
15080
+ "initialize"
15081
+ ]);
15082
+ var KeychainAuditEntrySchema = object({
15083
+ id: string(),
15084
+ at: number(),
15085
+ action: KeychainAuditActionSchema,
15086
+ /** `addon:<id>` | `user:<id>` | `system`. */
15087
+ actor: string(),
15088
+ ref: string().nullable(),
15089
+ detail: string().nullable()
15090
+ });
15091
+ 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({
15092
+ value: string(),
15093
+ version: number().int()
15094
+ }), {
15095
+ kind: "mutation",
15096
+ auth: "admin",
15097
+ caller: "required"
15098
+ }), method(object({ refs: array(string().min(1)).max(1e3) }), object({
15099
+ deleted: array(string()).readonly(),
15100
+ kept: array(object({
15101
+ ref: string(),
15102
+ usage: KeychainSecretUsageSchema
15103
+ })).readonly()
15104
+ }), {
15105
+ kind: "mutation",
15106
+ auth: "admin",
15107
+ caller: "required"
15108
+ }), method(object({}), object({
15109
+ fingerprint: string(),
15110
+ keySource: KeychainKeySourceSchema
15111
+ }), {
15112
+ kind: "mutation",
15113
+ auth: "admin",
15114
+ caller: "required"
15115
+ }), method(object({}), object({ purged: number().int() }), {
15116
+ kind: "mutation",
15117
+ auth: "admin",
15118
+ caller: "required"
15119
+ }), method(object({}), object({
15120
+ key: string(),
15121
+ fingerprint: string(),
15122
+ keySource: KeychainKeySourceSchema
15123
+ }), {
15124
+ kind: "mutation",
15125
+ auth: "admin",
15126
+ caller: "required"
15127
+ }), method(object({ confirm: literal("RESET KEYCHAIN") }), object({
15128
+ archivedCollection: string().nullable(),
15129
+ archived: number().int()
15130
+ }), {
15131
+ kind: "mutation",
15132
+ auth: "admin",
15133
+ caller: "required"
15134
+ });
14278
15135
  method(_void(), object({
14279
15136
  id: string(),
14280
15137
  name: string(),
@@ -15337,7 +16194,11 @@ var DeviceRegisterPayloadSchema = object({
15337
16194
  var DeviceRemovePayloadSchema = object({ deviceId: number() });
15338
16195
  var DevicePersistConfigPayloadSchema = object({
15339
16196
  deviceId: number(),
15340
- data: record(string(), unknown())
16197
+ data: record(string(), unknown()),
16198
+ /** The paths the device's schema declares secret (D783), when the caller
16199
+ * knows them — those values are kept in the keychain, not the blob (D787).
16200
+ * Absent: the paths the device last reported at `registerDevice` apply. */
16201
+ secretConfigPaths: array(string()).optional()
15341
16202
  });
15342
16203
  /** What a migration actually did, per switch. `unreachable` is a first-class
15343
16204
  * answer: a camera that could not be asked is not a camera that was silenced. */
@@ -22332,8 +23193,23 @@ var RetrainMacroClassSchema = _enum([
22332
23193
  ]);
22333
23194
  /** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
22334
23195
  var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
22335
- /** Did a human draw this box, or did the assist propose it? */
22336
- var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
23196
+ /**
23197
+ * Where a box came from. A machine-sourced box is a DRAFT on the page until
23198
+ * the operator confirms it; once saved it keeps its source as provenance.
23199
+ *
23200
+ * - `operator` — drawn by hand.
23201
+ * - `assist` — proposed by a detector run on the copy (the camera's own
23202
+ * model, or a larger teacher model).
23203
+ * - `pipeline` — the box the live pipeline actually drew on that frame, read
23204
+ * back from the track store (never a re-run).
23205
+ * - `llm` — proposed by a vision-capable LLM profile.
23206
+ */
23207
+ var RetrainAnnotationSourceSchema = _enum([
23208
+ "operator",
23209
+ "assist",
23210
+ "pipeline",
23211
+ "llm"
23212
+ ]);
22337
23213
  /** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
22338
23214
  var RetrainBboxSchema = object({
22339
23215
  x: number(),
@@ -22351,7 +23227,8 @@ var RetrainBboxSchema = object({
22351
23227
  */
22352
23228
  var RetrainAnnotationSchema = object({
22353
23229
  id: string(),
22354
- trackId: string(),
23230
+ /** The frame's track. Absent on a recording still, which has none. */
23231
+ trackId: string().optional(),
22355
23232
  deviceId: number(),
22356
23233
  /** The COPY in retrain storage — never the source track's media key. */
22357
23234
  mediaKey: string(),
@@ -22400,13 +23277,47 @@ var RetrainFrameCandidateSchema = object({
22400
23277
  * fail, whatever became of the original. */
22401
23278
  copied: boolean()
22402
23279
  });
23280
+ /**
23281
+ * One box the live pipeline drew on a copied frame — what a correction starts
23282
+ * from. Derived when the frame is listed, from the track store: the subject
23283
+ * box recorded on the source media row when there is one, else the track
23284
+ * position nearest the frame's capture time. Never stored as an annotation
23285
+ * until the operator confirms it.
23286
+ */
23287
+ var RetrainPipelineBoxSchema = object({
23288
+ /** Normalised `[0,1]` against the full frame. */
23289
+ bbox: RetrainBboxSchema,
23290
+ macroClass: RetrainMacroClassSchema,
23291
+ label: string().optional(),
23292
+ subLabel: string().optional(),
23293
+ /** The detection score of the position used, when it was recorded. */
23294
+ score: number().optional(),
23295
+ /** The root model that produced the track, when it was recorded. */
23296
+ modelId: string().optional(),
23297
+ trackId: string(),
23298
+ /** `subject-box`: recorded on this very frame. `position`: the track's
23299
+ * position nearest the capture time, `deltaMs` away. */
23300
+ basis: _enum(["subject-box", "position"]),
23301
+ deltaMs: number()
23302
+ });
23303
+ /**
23304
+ * Where a retrain frame came from: a TRACK's stored media (copy-on-select),
23305
+ * or the RECORDING at an instant — the frame a model saw nothing in has no
23306
+ * track at all.
23307
+ */
23308
+ var RetrainFrameOriginSchema = _enum(["track", "recording"]);
22403
23309
  /** A frame the dataset OWNS: bytes copied at selection time. */
22404
23310
  var RetrainFrameSchema = object({
22405
23311
  frameId: string(),
22406
23312
  deviceId: number(),
22407
- trackId: string(),
22408
- /** Provenance only. It may already point at nothing — that is expected. */
22409
- sourceMediaId: number().int(),
23313
+ origin: RetrainFrameOriginSchema,
23314
+ /** The track it was copied from. Absent on a recording still. */
23315
+ trackId: string().optional(),
23316
+ /** Provenance only. It may already point at nothing — that is expected.
23317
+ * Absent on a recording still. */
23318
+ sourceMediaId: number().int().optional(),
23319
+ /** A recording still: the playhead instant it was taken at. */
23320
+ epochMs: number().optional(),
22410
23321
  sourceKind: MediaFileKindEnum,
22411
23322
  sizeBytes: number().int(),
22412
23323
  width: number().int(),
@@ -22417,7 +23328,12 @@ var RetrainFrameSchema = object({
22417
23328
  /** The operator's "every subject in this frame is boxed". Detector
22418
23329
  * evaluation keeps only complete frames: an unboxed subject would count as
22419
23330
  * a false positive. */
22420
- complete: boolean()
23331
+ complete: boolean(),
23332
+ /** The boxes the pipeline drew on this frame. Present on list answers;
23333
+ * empty when nothing the track store holds can be placed on it. */
23334
+ pipelineBoxes: array(RetrainPipelineBoxSchema).readonly().optional(),
23335
+ /** The named collections this frame belongs to. Present on list answers. */
23336
+ collectionIds: array(string()).readonly().optional()
22421
23337
  });
22422
23338
  /** Why a copy-on-select could not be honoured — named, never a silent skip. */
22423
23339
  var RetrainCopyRefusalSchema = _enum([
@@ -22448,6 +23364,8 @@ var RetrainFrameListSchema = object({
22448
23364
  var RetrainExportFilterSchema = object({
22449
23365
  deviceIds: array(number()).optional(),
22450
23366
  frameIds: array(string()).max(2e4).optional(),
23367
+ /** Only frames in at least one of these named collections. */
23368
+ collectionIds: array(string()).max(200).optional(),
22451
23369
  batchId: string().optional(),
22452
23370
  /** Only frames the operator marked complete (all subjects boxed). */
22453
23371
  completeOnly: boolean().optional()
@@ -22512,6 +23430,109 @@ var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
22512
23430
  reason: string(),
22513
23431
  detail: string().optional()
22514
23432
  })]);
23433
+ /** A named set over the retrain corpus — membership only, it owns no pixels. */
23434
+ var RetrainCollectionSchema = object({
23435
+ id: string(),
23436
+ name: string(),
23437
+ createdAt: number(),
23438
+ frameCount: number().int()
23439
+ });
23440
+ var RetrainCollectionCreateResultSchema = discriminatedUnion("kind", [object({
23441
+ kind: literal("created"),
23442
+ collection: RetrainCollectionSchema
23443
+ }), object({
23444
+ kind: literal("refused"),
23445
+ reason: _enum(["empty-name", "duplicate-name"])
23446
+ })]);
23447
+ var RetrainCollectionRenameResultSchema = discriminatedUnion("kind", [object({
23448
+ kind: literal("renamed"),
23449
+ collection: RetrainCollectionSchema
23450
+ }), object({
23451
+ kind: literal("refused"),
23452
+ reason: _enum([
23453
+ "empty-name",
23454
+ "duplicate-name",
23455
+ "unknown-collection"
23456
+ ])
23457
+ })]);
23458
+ /**
23459
+ * The outcome of adding the frame at a recording instant. Every refusal is
23460
+ * named: `still-unavailable` (not recording, a gap, or an undecodable frame —
23461
+ * `detail` says which), `still-busy` (the recorder is at its decode bound; try
23462
+ * again), `unreadable-image`, `write-failed`, `unknown-collection`.
23463
+ */
23464
+ var AddRetrainFrameFromRecordingResultSchema = discriminatedUnion("kind", [object({
23465
+ kind: literal("added"),
23466
+ collectionId: string(),
23467
+ frame: RetrainFrameSchema,
23468
+ /** The moment was already in the dataset; nothing was decoded. */
23469
+ reused: boolean()
23470
+ }), object({
23471
+ kind: literal("refused"),
23472
+ reason: _enum([
23473
+ "unknown-collection",
23474
+ "still-unavailable",
23475
+ "still-busy",
23476
+ "unreadable-image",
23477
+ "write-failed"
23478
+ ]),
23479
+ detail: string().optional()
23480
+ })]);
23481
+ /** A vision LLM profile the AI assist may use, and where it sends the frame. */
23482
+ var RetrainAssistProfileSchema = object({
23483
+ id: string(),
23484
+ name: string(),
23485
+ model: string(),
23486
+ kind: string(),
23487
+ /** `true` when the frame would leave the house (a cloud vendor, or an
23488
+ * endpoint that is not on the local network). The page confirms each such
23489
+ * run, and the provider refuses one without `allowCloud`. */
23490
+ leavesHouse: boolean()
23491
+ });
23492
+ /** A box the AI assist proposed: a draft, plus the model's own reason. */
23493
+ var RetrainLlmProposalSchema = RetrainAnnotationDraftSchema.extend({ reason: string() });
23494
+ /** What the AI assist said about a box that was already on the canvas. */
23495
+ var RetrainLlmReviewSchema = object({
23496
+ /** The `ref` the page sent the box under. */
23497
+ ref: string(),
23498
+ verdict: _enum([
23499
+ "confirmed",
23500
+ "wrong-class",
23501
+ "not-an-object",
23502
+ "uncertain"
23503
+ ]),
23504
+ suggestedClass: RetrainMacroClassSchema.optional(),
23505
+ reason: string()
23506
+ });
23507
+ /**
23508
+ * The AI assist's answer. `proposed` carries drafts (`source: 'llm'`, the
23509
+ * model and its confidence) and reviews; items the model got wrong (a box off
23510
+ * the frame, an unasked class, an unknown ref) are dropped and counted in
23511
+ * `rejected`. A whole answer that is not the schema is `malformed-answer`.
23512
+ */
23513
+ var RetrainLlmAssistResultSchema = discriminatedUnion("kind", [object({
23514
+ kind: literal("proposed"),
23515
+ profileId: string(),
23516
+ model: string(),
23517
+ /** The frame was sent outside the house for this answer. */
23518
+ leftHouse: boolean(),
23519
+ proposals: array(RetrainLlmProposalSchema).readonly(),
23520
+ reviews: array(RetrainLlmReviewSchema).readonly(),
23521
+ rejected: number().int()
23522
+ }), object({
23523
+ kind: literal("refused"),
23524
+ reason: _enum([
23525
+ "unknown-frame",
23526
+ "unreadable-frame",
23527
+ "unknown-profile",
23528
+ "profile-disabled",
23529
+ "no-vision",
23530
+ "cloud-not-confirmed",
23531
+ "llm-error",
23532
+ "malformed-answer"
23533
+ ]),
23534
+ detail: string().optional()
23535
+ })]);
22515
23536
  /** The outcome of a lifecycle move owned by the retrain page. */
22516
23537
  var RetrainTransitionResultSchema = object({
22517
23538
  trackId: string(),
@@ -23229,7 +24250,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema$1).r
23229
24250
  auth: "admin"
23230
24251
  }), method(object({
23231
24252
  deviceId: number(),
23232
- trackId: string(),
24253
+ trackId: string().optional(),
23233
24254
  frameId: string()
23234
24255
  }), object({
23235
24256
  removed: boolean(),
@@ -23246,7 +24267,8 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema$1).r
23246
24267
  auth: "admin"
23247
24268
  }), method(object({
23248
24269
  deviceId: number(),
23249
- trackId: string(),
24270
+ /** The frame's track, when it has one — checked against the frame. */
24271
+ trackId: string().optional(),
23250
24272
  frameId: string(),
23251
24273
  subject: RetrainAssistSubjectSchema,
23252
24274
  /** Which node runs it. Absent ⇒ wherever an unowned call lands. */
@@ -23254,12 +24276,16 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema$1).r
23254
24276
  }), RetrainAssistResultSchema, {
23255
24277
  kind: "mutation",
23256
24278
  auth: "admin"
23257
- }), method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
24279
+ }), method(object({
24280
+ trackId: string().optional(),
24281
+ frameIds: array(string()).max(2e3).optional()
24282
+ }), array(RetrainAnnotationSchema).readonly(), {
23258
24283
  kind: "query",
23259
24284
  auth: "admin"
23260
24285
  }), method(object({
23261
24286
  deviceId: number(),
23262
- trackId: string(),
24287
+ /** The frame's track, when it has one — checked against the frame. */
24288
+ trackId: string().optional(),
23263
24289
  frameId: string(),
23264
24290
  annotations: array(RetrainAnnotationDraftSchema)
23265
24291
  }), array(RetrainAnnotationSchema).readonly(), {
@@ -23282,7 +24308,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema$1).r
23282
24308
  auth: "admin"
23283
24309
  }), method(object({
23284
24310
  deviceId: number(),
23285
- trackId: string(),
24311
+ trackId: string().optional(),
23286
24312
  frameId: string(),
23287
24313
  complete: boolean()
23288
24314
  }), RetrainFrameSchema, {
@@ -23296,6 +24322,74 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema$1).r
23296
24322
  kind: "mutation",
23297
24323
  auth: "admin",
23298
24324
  timeoutMs: 60 * 6e4
24325
+ }), method(object({}), array(RetrainAssistProfileSchema).readonly(), {
24326
+ kind: "query",
24327
+ auth: "admin"
24328
+ }), method(object({
24329
+ deviceId: number(),
24330
+ trackId: string().optional(),
24331
+ frameId: string(),
24332
+ profileId: string(),
24333
+ classes: array(RetrainMacroClassSchema).min(1),
24334
+ boxes: array(object({
24335
+ ref: string(),
24336
+ bbox: RetrainBboxSchema,
24337
+ macroClass: RetrainMacroClassSchema,
24338
+ label: string().optional()
24339
+ })).max(50),
24340
+ allowCloud: boolean()
24341
+ }), RetrainLlmAssistResultSchema, {
24342
+ kind: "mutation",
24343
+ auth: "admin",
24344
+ timeoutMs: 18e4
24345
+ }), method(object({ name: string().max(120) }), RetrainCollectionCreateResultSchema, {
24346
+ kind: "mutation",
24347
+ auth: "admin"
24348
+ }), method(object({}), array(RetrainCollectionSchema).readonly(), {
24349
+ kind: "query",
24350
+ auth: "admin"
24351
+ }), method(object({
24352
+ id: string(),
24353
+ name: string().max(120)
24354
+ }), RetrainCollectionRenameResultSchema, {
24355
+ kind: "mutation",
24356
+ auth: "admin"
24357
+ }), method(object({ id: string() }), object({
24358
+ deleted: boolean(),
24359
+ removedFrames: number().int()
24360
+ }), {
24361
+ kind: "mutation",
24362
+ auth: "admin"
24363
+ }), method(object({
24364
+ collectionId: string(),
24365
+ deviceId: number(),
24366
+ epochMs: number()
24367
+ }), AddRetrainFrameFromRecordingResultSchema, {
24368
+ kind: "mutation",
24369
+ auth: "admin",
24370
+ timeoutMs: 6e4
24371
+ }), method(object({
24372
+ collectionId: string(),
24373
+ frameIds: array(string()).min(1).max(500)
24374
+ }), object({
24375
+ added: number().int(),
24376
+ alreadyMember: number().int(),
24377
+ unknownFrameIds: array(string()).readonly()
24378
+ }), {
24379
+ kind: "mutation",
24380
+ auth: "admin"
24381
+ }), method(object({
24382
+ collectionId: string(),
24383
+ frameId: string()
24384
+ }), object({
24385
+ removed: boolean(),
24386
+ deletedFrame: boolean()
24387
+ }), {
24388
+ kind: "mutation",
24389
+ auth: "admin"
24390
+ }), method(object({ collectionId: string() }), array(RetrainFrameSchema).readonly(), {
24391
+ kind: "query",
24392
+ auth: "admin"
23299
24393
  }), method(object({
23300
24394
  /** Which of the three event tables owns the row. */
23301
24395
  ownerType: EventOwnerTypeSchema,
@@ -23919,13 +25013,31 @@ var RecommendationReasonSchema = discriminatedUnion("kind", [
23919
25013
  object({
23920
25014
  kind: literal("no-family-build"),
23921
25015
  format: ModelFormatSchema$1
25016
+ }),
25017
+ object({
25018
+ kind: literal("kept-custom-pin"),
25019
+ modelId: string(),
25020
+ deviceKey: string()
23922
25021
  })
23923
25022
  ]);
25023
+ /** Why a device is left exactly as it is, whatever the plan would pick (D786). */
25024
+ var KeptReasonSchema = _enum(["custom-pin"]);
25025
+ /**
25026
+ * Where a choice's latency came from (D791): this node's own benchmark
25027
+ * result, or the hardware-class table. Measured on the node beats the table.
25028
+ */
25029
+ var LatencySourceSchema = discriminatedUnion("kind", [object({ kind: literal("table") }), object({
25030
+ kind: literal("node-benchmark"),
25031
+ resultId: string(),
25032
+ measuredAtMs: number()
25033
+ })]);
23924
25034
  var DeviceModelChoiceSchema = object({
23925
25035
  modelId: string(),
23926
25036
  p50Ms: number(),
23927
25037
  fps: number(),
23928
25038
  measured: boolean(),
25039
+ /** Absent on a plan from before D791 — read as the table. */
25040
+ source: LatencySourceSchema.optional(),
23929
25041
  /** Parallel sessions at the camera rate with the headroom kept free: ⌊fps × (1 − headroom) / camera fps⌋. */
23930
25042
  capacitySessions: number(),
23931
25043
  personRecall: number()
@@ -23936,6 +25048,8 @@ var DeviceRecommendationSchema = object({
23936
25048
  enabled: boolean(),
23937
25049
  available: boolean(),
23938
25050
  chosen: DeviceModelChoiceSchema.nullable(),
25051
+ /** Present when the device is kept as is: Apply writes nothing on it. */
25052
+ kept: KeptReasonSchema.optional(),
23939
25053
  reasons: array(RecommendationReasonSchema).readonly()
23940
25054
  });
23941
25055
  var NodeRecommendationSchema = object({
@@ -23989,6 +25103,17 @@ var AutoTuneWarningSchema = discriminatedUnion("kind", [
23989
25103
  kind: literal("coral-enabled"),
23990
25104
  nodeId: string(),
23991
25105
  deviceKey: string()
25106
+ }),
25107
+ object({
25108
+ kind: literal("custom-pin-kept"),
25109
+ nodeId: string(),
25110
+ deviceKey: string().nullable(),
25111
+ deviceId: number().nullable(),
25112
+ modelId: string()
25113
+ }),
25114
+ object({
25115
+ kind: literal("custom-registry-unreadable"),
25116
+ error: string()
23992
25117
  })
23993
25118
  ]);
23994
25119
  var AutoTuneOptionsSchema = object({
@@ -24167,13 +25292,14 @@ var SuggestSharesResultSchema = object({
24167
25292
  nodes: array(SuggestedNodeShareSchema).readonly()
24168
25293
  });
24169
25294
  /**
24170
- * Where the node and device shares come from (D750, D755).
24171
- * - `hardware` (default) — computed by the system from each node's static
24172
- * capacity (its accelerators at their Auto tune models, from the measured
24173
- * latency table), recomputed whenever a node or device joins, leaves, is
24174
- * switched on or off, or changes model. No load input.
24175
- * - `benchmark` — the same, from each node's measured capacity (node
24176
- * benchmark results); a node without results falls back to `hardware`.
25295
+ * Where the node and device shares come from (D750, D755, D791).
25296
+ * - `benchmark` (default since D791) — each node's capacity as its own node
25297
+ * benchmark measured it; a node without fresh results is sized from
25298
+ * `hardware` and says so. Measured on the node beats the table.
25299
+ * - `hardware` — computed from each node's static capacity only (its
25300
+ * accelerators at their Auto tune models, from the latency table).
25301
+ * Both are recomputed whenever a node or device joins, leaves, is switched
25302
+ * on or off, changes model, or a benchmark result is written. No load input.
24177
25303
  * - `manual` — the operator's stored sliders.
24178
25304
  * Either way a node or device that cannot take sessions gives its share to
24179
25305
  * the others in proportion; the stored values are never rewritten for that.
@@ -24222,6 +25348,362 @@ var EffectiveSharesSchema = object({
24222
25348
  nodeWeights: record(string(), number()),
24223
25349
  deviceWeights: record(string(), record(string(), number()))
24224
25350
  });
25351
+ /**
25352
+ * Wire shapes of the node benchmark (D791): the operator's plan, the
25353
+ * preflight the confirmation shows, the run the orchestrator owns, what the
25354
+ * measuring runner reports, and the result rows Auto tune and the automatic
25355
+ * shares read.
25356
+ *
25357
+ * ONE node at a time, owned by the pipeline orchestrator. While a run is
25358
+ * active the cluster is in BENCHMARK MODE for that node: it is not a
25359
+ * detection candidate, camera pins naming it are ignored so its sessions move
25360
+ * elsewhere, and both come back when the mode ends — after a crash or a hub
25361
+ * restart too (a persisted manifest, reconciled against the registry).
25362
+ *
25363
+ * Two measurements, both on the node, driven by its own runner (frames never
25364
+ * cross a process at frame rate, D9):
25365
+ * - `device` — one accelerator × one candidate detector, root only, open
25366
+ * loop: how many streams at the camera rate it carries inside the frame
25367
+ * interval;
25368
+ * - `pipeline` — the chosen detector plus the operator's detail steps
25369
+ * (faces, plates, classifiers, CLIP…), on varied frames from the bench
25370
+ * clip: how many parallel sessions the node carries before latency, drops,
25371
+ * CPU, RAM or the event loop give out.
25372
+ */
25373
+ /** Defaults and bounds, one place. */
25374
+ var NODE_BENCHMARK_DEFAULTS = {
25375
+ fpsPerStream: 10,
25376
+ budgetMinutes: 45,
25377
+ minBudgetMinutes: 10,
25378
+ maxBudgetMinutes: 120,
25379
+ /** A result older than this no longer beats the table. */
25380
+ staleAfterDays: 30,
25381
+ /** Warm-in per ramp step (not accounted). */
25382
+ settleSec: 5,
25383
+ /** Accounted window per ramp step. */
25384
+ measureSec: 20,
25385
+ /** Longest the drain may take before the run is refused. */
25386
+ drainTimeoutSec: 120,
25387
+ /** Candidate models per device in one run. */
25388
+ maxModelsPerDevice: 6
25389
+ };
25390
+ /** The two measurements a run can make. */
25391
+ var NodeBenchmarkPhaseSchema = _enum(["device", "pipeline"]);
25392
+ var NodeBenchmarkDevicePlanSchema = object({
25393
+ /** `<backend>:<device>` — `openvino:npu`, `coreml:all`, `cpu`. */
25394
+ deviceKey: string().min(1),
25395
+ /**
25396
+ * Root detector candidates for the `device` measurement, in the order
25397
+ * measured. The FIRST is the one the `pipeline` measurement runs.
25398
+ */
25399
+ modelIds: array(string().min(1)).min(1).max(NODE_BENCHMARK_DEFAULTS.maxModelsPerDevice)
25400
+ });
25401
+ /** What the operator confirmed. */
25402
+ var NodeBenchmarkPlanSchema = object({
25403
+ nodeId: string().min(1),
25404
+ devices: array(NodeBenchmarkDevicePlanSchema).min(1),
25405
+ /** Which measurements run. At least one. */
25406
+ phases: array(NodeBenchmarkPhaseSchema).min(1).default(["device", "pipeline"]),
25407
+ /**
25408
+ * Detail steps (catalog step ids under the root: faces, plates,
25409
+ * classifiers, CLIP…) the `pipeline` measurement enables. The root
25410
+ * detector always runs.
25411
+ */
25412
+ steps: array(string().min(1)).default([]),
25413
+ /** Offered rate of one stream / session. */
25414
+ fpsPerStream: number().int().min(1).max(30).default(NODE_BENCHMARK_DEFAULTS.fpsPerStream),
25415
+ /** Hard deadline: the mode ends and the cluster is restored when it passes. */
25416
+ budgetMinutes: number().int().min(NODE_BENCHMARK_DEFAULTS.minBudgetMinutes).max(NODE_BENCHMARK_DEFAULTS.maxBudgetMinutes).default(NODE_BENCHMARK_DEFAULTS.budgetMinutes),
25417
+ /** Keep the models the run downloaded (default: delete what was not installed before). */
25418
+ keepDownloadedModels: boolean().default(false),
25419
+ /** Start although the other nodes' capacity does not cover the sessions running now. */
25420
+ acceptInsufficientHeadroom: boolean().default(false)
25421
+ });
25422
+ var NodeBenchmarkRefusalSchema = object({
25423
+ kind: _enum([
25424
+ "benchmark-active",
25425
+ "node-offline",
25426
+ "unknown-device",
25427
+ "no-phase",
25428
+ "insufficient-headroom"
25429
+ ]),
25430
+ detail: string(),
25431
+ overridable: boolean()
25432
+ });
25433
+ /** A camera whose detection leaves the node for the run. */
25434
+ var NodeBenchmarkMovingCameraSchema = object({
25435
+ deviceId: number(),
25436
+ /** Placed on the node now (a standing attach or a running session). */
25437
+ placedOnNode: boolean(),
25438
+ /** A node pin naming this node, ignored for the run. */
25439
+ pinnedToNode: boolean()
25440
+ });
25441
+ /** A camera that cannot leave: no other node can obtain its frames. */
25442
+ var NodeBenchmarkStayingCameraSchema = object({
25443
+ deviceId: number(),
25444
+ reason: literal("only-frame-source")
25445
+ });
25446
+ var NodeBenchmarkPreflightSchema = object({
25447
+ nodeId: string(),
25448
+ online: boolean(),
25449
+ moving: array(NodeBenchmarkMovingCameraSchema).readonly(),
25450
+ /** Keep running on the node during the run; the result is flagged contaminated. */
25451
+ staying: array(NodeBenchmarkStayingCameraSchema).readonly(),
25452
+ estimatedDurationMs: number(),
25453
+ /** The run ends and the cluster is restored at the latest after this long. */
25454
+ budgetMs: number(),
25455
+ phases: array(NodeBenchmarkPhaseSchema).readonly(),
25456
+ steps: array(string()).readonly(),
25457
+ devices: array(NodeBenchmarkDevicePlanSchema).readonly(),
25458
+ /** Parallel sessions the cluster runs now, and what the other nodes can carry. */
25459
+ headroom: object({
25460
+ sessionsNow: number(),
25461
+ otherNodesCapacitySessions: number().nullable()
25462
+ }),
25463
+ refusals: array(NodeBenchmarkRefusalSchema).readonly(),
25464
+ canStart: boolean()
25465
+ });
25466
+ /** Where the measuring runner is. */
25467
+ var NodeBenchProgressSchema = object({
25468
+ phase: _enum([
25469
+ "preparing",
25470
+ "device",
25471
+ "pipeline",
25472
+ "cleanup"
25473
+ ]),
25474
+ deviceKey: string().nullable(),
25475
+ modelId: string().nullable(),
25476
+ /** Streams (device) or sessions (pipeline) of the step running now. */
25477
+ load: number().nullable(),
25478
+ cellsDone: number(),
25479
+ cellsTotal: number(),
25480
+ message: string()
25481
+ });
25482
+ /** What limited a ramp. `none` = the ramp ran out of steps still passing. */
25483
+ var NodeBenchLimitSchema = _enum([
25484
+ "p95",
25485
+ "drops",
25486
+ "errors",
25487
+ "stall",
25488
+ "cpu",
25489
+ "ram",
25490
+ "none"
25491
+ ]);
25492
+ var NodeBenchRampStepSchema = object({
25493
+ load: number(),
25494
+ pass: boolean(),
25495
+ p95Ms: number(),
25496
+ dropRatio: number(),
25497
+ /** Completed frames per second over the accounted window. */
25498
+ fps: number(),
25499
+ cpuPct: number().nullable(),
25500
+ ramFreePct: number().nullable(),
25501
+ failReasons: array(string()).readonly()
25502
+ });
25503
+ var NodeBenchCellSchema = discriminatedUnion("phase", [object({
25504
+ phase: literal("device"),
25505
+ deviceKey: string(),
25506
+ modelId: string(),
25507
+ /** Single-stream latency (the K = 1 step). */
25508
+ p50Ms: number(),
25509
+ p95Ms: number(),
25510
+ p99Ms: number(),
25511
+ /** Frames per second sustained at the last passing step. */
25512
+ fps: number(),
25513
+ /** Streams at the camera rate the device carried inside the frame interval; 0 = not even one. */
25514
+ kMax: number(),
25515
+ limitedBy: NodeBenchLimitSchema,
25516
+ /** First call (model load + warm-up). */
25517
+ warmupMs: number(),
25518
+ ramp: array(NodeBenchRampStepSchema).readonly()
25519
+ }), object({
25520
+ phase: literal("pipeline"),
25521
+ deviceKey: string(),
25522
+ modelId: string(),
25523
+ steps: array(string()).readonly(),
25524
+ /** Parallel sessions at the camera rate the node carried; 0 = not even one. */
25525
+ maxSessions: number(),
25526
+ limitedBy: NodeBenchLimitSchema,
25527
+ /** Detail steps that produced at least one result on the clip — a step that never fired measured nothing. */
25528
+ stepsFired: array(string()).readonly(),
25529
+ warmupMs: number(),
25530
+ ramp: array(NodeBenchRampStepSchema).readonly()
25531
+ })]);
25532
+ /**
25533
+ * What a result was measured ON. A result whose fingerprint no longer matches
25534
+ * the node no longer beats the table.
25535
+ */
25536
+ var NodeBenchFingerprintSchema = object({
25537
+ pipelineVersion: string().nullable(),
25538
+ cpuModel: string().nullable(),
25539
+ ramGb: number().nullable(),
25540
+ os: string().nullable(),
25541
+ /** Per device: the pool the cells ran on (runtime + tuning). */
25542
+ pools: record(string(), object({
25543
+ runtime: string(),
25544
+ numWorkers: number(),
25545
+ concurrency: number(),
25546
+ batchMode: string()
25547
+ })).default({})
25548
+ });
25549
+ /** The frames the run used. */
25550
+ var NodeBenchClipSchema = object({
25551
+ /** `camstack-bench-v1` or `reference-images` (the fallback when the clip is not reachable). */
25552
+ id: string(),
25553
+ revision: string().nullable(),
25554
+ frames: number(),
25555
+ width: number(),
25556
+ height: number()
25557
+ });
25558
+ var NodeBenchRunnerInputSchema = object({
25559
+ runId: string().min(1),
25560
+ devices: array(NodeBenchmarkDevicePlanSchema).min(1),
25561
+ phases: array(NodeBenchmarkPhaseSchema).min(1),
25562
+ /** The full detail step tree root for the `pipeline` measurement is built on the node from these ids. */
25563
+ steps: array(string()).default([]),
25564
+ fpsPerStream: number().int().min(1).max(30),
25565
+ /** The runner stops on its own past this instant, whatever the hub does. */
25566
+ deadlineAtMs: number(),
25567
+ settleSec: number().min(0).max(60).default(NODE_BENCHMARK_DEFAULTS.settleSec),
25568
+ measureSec: number().min(2).max(300).default(NODE_BENCHMARK_DEFAULTS.measureSec),
25569
+ keepDownloadedModels: boolean().default(false)
25570
+ });
25571
+ var NodeBenchRunnerStateSchema = _enum([
25572
+ "running",
25573
+ "done",
25574
+ "failed",
25575
+ "aborted"
25576
+ ]);
25577
+ var NodeBenchRunnerStatusSchema = object({
25578
+ runId: string(),
25579
+ nodeId: string(),
25580
+ state: NodeBenchRunnerStateSchema,
25581
+ progress: NodeBenchProgressSchema.nullable(),
25582
+ /** Every finished cell so far — a run stopped half way keeps what it measured. */
25583
+ cells: array(NodeBenchCellSchema).readonly(),
25584
+ fingerprint: NodeBenchFingerprintSchema.nullable(),
25585
+ clip: NodeBenchClipSchema.nullable(),
25586
+ /** Cameras seen running on the node during the run (they share its pools). */
25587
+ activeCameraIds: array(number()).readonly(),
25588
+ /** Models the run downloaded and then deleted (not installed before). */
25589
+ deletedModels: array(string()).readonly(),
25590
+ error: string().nullable(),
25591
+ startedAtMs: number(),
25592
+ finishedAtMs: number().nullable()
25593
+ });
25594
+ var NodeBenchmarkStateSchema = _enum([
25595
+ "draining",
25596
+ "measuring",
25597
+ "restoring",
25598
+ "done",
25599
+ "failed",
25600
+ "aborted"
25601
+ ]);
25602
+ /** Why a run ended. */
25603
+ var NodeBenchmarkEndReasonSchema = _enum([
25604
+ "completed",
25605
+ "operator-abort",
25606
+ "deadline",
25607
+ "node-lost",
25608
+ "runner-lost",
25609
+ "hub-restart",
25610
+ "drain-timeout",
25611
+ "failed"
25612
+ ]);
25613
+ var NodeBenchmarkRunSchema = object({
25614
+ runId: string(),
25615
+ nodeId: string(),
25616
+ state: NodeBenchmarkStateSchema,
25617
+ plan: NodeBenchmarkPlanSchema,
25618
+ startedAtMs: number(),
25619
+ deadlineAtMs: number(),
25620
+ finishedAtMs: number().nullable(),
25621
+ endReason: NodeBenchmarkEndReasonSchema.nullable(),
25622
+ error: string().nullable(),
25623
+ /** Cameras moved off the node for the run. */
25624
+ moved: array(number()).readonly(),
25625
+ /** Cameras that stayed (their only frame source is the node). */
25626
+ staying: array(number()).readonly(),
25627
+ /** Something else ran on the node's pools during the run. */
25628
+ contaminated: boolean(),
25629
+ progress: NodeBenchProgressSchema.nullable(),
25630
+ resultIds: array(string()).readonly()
25631
+ });
25632
+ /** `getNodeBenchmarkStatus`: the active run (or the last one), and whether benchmark mode is on. */
25633
+ var NodeBenchmarkStatusSchema = object({
25634
+ /** The node the cluster is in benchmark mode for; null = none. */
25635
+ modeNodeId: string().nullable(),
25636
+ run: NodeBenchmarkRunSchema.nullable()
25637
+ });
25638
+ /**
25639
+ * One stored measurement — the only authority Auto tune and the automatic
25640
+ * shares read measured capacity from. Written by the orchestrator when a run
25641
+ * ends, never by anyone else.
25642
+ */
25643
+ var NodeBenchmarkResultSchema = object({
25644
+ id: string(),
25645
+ runId: string(),
25646
+ nodeId: string(),
25647
+ cell: NodeBenchCellSchema,
25648
+ fpsPerStream: number(),
25649
+ fingerprint: NodeBenchFingerprintSchema.nullable(),
25650
+ clip: NodeBenchClipSchema.nullable(),
25651
+ /** Other work shared the node's pools (a camera that could not move, or the hub's own duties). */
25652
+ contaminated: boolean(),
25653
+ stayingCameras: array(number()).readonly(),
25654
+ /** The run ended early; the cell itself completed. */
25655
+ partialRun: boolean(),
25656
+ measuredAtMs: number()
25657
+ });
25658
+ var ListNodeBenchmarkResultsInputSchema = object({
25659
+ nodeId: string().optional(),
25660
+ deviceKey: string().optional()
25661
+ });
25662
+ /** A result with its standing today: does it still beat the table? */
25663
+ var NodeBenchmarkResultViewSchema = NodeBenchmarkResultSchema.extend({
25664
+ /** `fresh` beats the table; `stale` falls back to it; `superseded` = a newer row covers the same cell. */
25665
+ standing: _enum([
25666
+ "fresh",
25667
+ "stale",
25668
+ "superseded"
25669
+ ]),
25670
+ staleWhy: _enum([
25671
+ "age",
25672
+ "runtime",
25673
+ "pool-tuning"
25674
+ ]).nullable()
25675
+ });
25676
+ object({
25677
+ runId: string(),
25678
+ nodeId: string(),
25679
+ startedAtMs: number(),
25680
+ deadlineAtMs: number(),
25681
+ plan: NodeBenchmarkPlanSchema,
25682
+ moved: array(number()).readonly(),
25683
+ staying: array(number()).readonly()
25684
+ });
25685
+ /** Candidate detectors per device, for the plan editor. */
25686
+ var NodeBenchmarkCandidatesSchema = object({
25687
+ nodeId: string(),
25688
+ reachable: boolean(),
25689
+ devices: array(object({
25690
+ deviceKey: string(),
25691
+ enabled: boolean(),
25692
+ available: boolean(),
25693
+ /** Ordered: what Auto tune would pick first, then the rest of the family by accuracy. */
25694
+ suggested: array(string()).readonly(),
25695
+ /** Every root detector the device has a build for. */
25696
+ all: array(string()).readonly(),
25697
+ /** The model the device runs now, when pinned. */
25698
+ currentModelId: string().nullable()
25699
+ })).readonly(),
25700
+ /** Detail steps the node's catalog offers under the root (for the `pipeline` measurement). */
25701
+ steps: array(object({
25702
+ id: string(),
25703
+ name: string(),
25704
+ slot: string()
25705
+ })).readonly()
25706
+ });
24225
25707
  object({
24226
25708
  activeCameras: number(),
24227
25709
  throttledCameras: number(),
@@ -25310,7 +26792,16 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
25310
26792
  }), ParkedTrackFrameSchema.nullable()), method(object({
25311
26793
  deviceId: number(),
25312
26794
  trackId: string()
25313
- }), object({ released: number().int().nonnegative() }), { kind: "mutation" });
26795
+ }), object({ released: number().int().nonnegative() }), { kind: "mutation" }), method(NodeBenchRunnerInputSchema, object({
26796
+ accepted: literal(true),
26797
+ runId: string()
26798
+ }), {
26799
+ kind: "mutation",
26800
+ auth: "admin"
26801
+ }), method(object({ runId: string() }), NodeBenchRunnerStatusSchema.nullable(), { auth: "admin" }), method(object({ runId: string() }), object({ aborted: boolean() }), {
26802
+ kind: "mutation",
26803
+ auth: "admin"
26804
+ });
25314
26805
  var CameraPipelineConfigSchema = object({
25315
26806
  engine: PipelineEngineChoiceSchema.optional(),
25316
26807
  steps: array(PipelineStepInputSchema).readonly(),
@@ -25949,6 +27440,21 @@ method(object({
25949
27440
  }), method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }), method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }), method(_void(), EffectiveSharesSchema, { auth: "admin" }), method(SetShareSourceInputSchema, object({ success: literal(true) }), {
25950
27441
  kind: "mutation",
25951
27442
  auth: "admin"
27443
+ }), method(NodeBenchmarkPlanSchema, NodeBenchmarkPreflightSchema, { auth: "admin" }), method(NodeBenchmarkPlanSchema, discriminatedUnion("kind", [object({
27444
+ kind: literal("started"),
27445
+ runId: string()
27446
+ }), object({
27447
+ kind: literal("refused"),
27448
+ preflight: NodeBenchmarkPreflightSchema
27449
+ })]), {
27450
+ kind: "mutation",
27451
+ auth: "admin"
27452
+ }), method(_void(), NodeBenchmarkStatusSchema, { auth: "admin" }), method(object({ runId: string() }), object({ aborted: boolean() }), {
27453
+ kind: "mutation",
27454
+ auth: "admin"
27455
+ }), method(object({ nodeId: string() }), NodeBenchmarkCandidatesSchema, { auth: "admin" }), method(ListNodeBenchmarkResultsInputSchema, array(NodeBenchmarkResultViewSchema).readonly(), { auth: "admin" }), method(object({ id: string() }), object({ deleted: boolean() }), {
27456
+ kind: "mutation",
27457
+ auth: "admin"
25952
27458
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
25953
27459
  deviceId: number(),
25954
27460
  addonId: string(),
@@ -31062,6 +32568,9 @@ var CreateIntegrationInputSchema = object({
31062
32568
  enabled: boolean().optional(),
31063
32569
  info: record(string(), unknown()).optional(),
31064
32570
  settings: record(string(), unknown()).optional(),
32571
+ /** Keys of `settings` the form's schema declares secret (by type) — kept in
32572
+ * the keychain, not the table (D787). */
32573
+ secretKeys: array(string()).optional(),
31065
32574
  skipRestart: boolean().optional()
31066
32575
  });
31067
32576
  var UpdateIntegrationInputSchema = object({
@@ -31085,7 +32594,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
31085
32594
  auth: "admin"
31086
32595
  }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
31087
32596
  id: string(),
31088
- settings: record(string(), unknown())
32597
+ settings: record(string(), unknown()),
32598
+ /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
32599
+ secretKeys: array(string()).optional()
31089
32600
  }), object({ success: boolean() }), {
31090
32601
  kind: "mutation",
31091
32602
  auth: "admin"
@@ -39813,6 +41324,90 @@ Object.freeze({
39813
41324
  addonId: null,
39814
41325
  access: "create"
39815
41326
  },
41327
+ "keychain.delete": {
41328
+ capName: "keychain",
41329
+ capScope: "system",
41330
+ addonId: null,
41331
+ access: "delete"
41332
+ },
41333
+ "keychain.getState": {
41334
+ capName: "keychain",
41335
+ capScope: "system",
41336
+ addonId: null,
41337
+ access: "view"
41338
+ },
41339
+ "keychain.listMeta": {
41340
+ capName: "keychain",
41341
+ capScope: "system",
41342
+ addonId: null,
41343
+ access: "view"
41344
+ },
41345
+ "keychain.put": {
41346
+ capName: "keychain",
41347
+ capScope: "system",
41348
+ addonId: null,
41349
+ access: "create"
41350
+ },
41351
+ "keychain.resolve": {
41352
+ capName: "keychain",
41353
+ capScope: "system",
41354
+ addonId: null,
41355
+ access: "view"
41356
+ },
41357
+ "keychainAdmin.deleteOrphans": {
41358
+ capName: "keychain-admin",
41359
+ capScope: "system",
41360
+ addonId: null,
41361
+ access: "delete"
41362
+ },
41363
+ "keychainAdmin.downloadRecoveryKey": {
41364
+ capName: "keychain-admin",
41365
+ capScope: "system",
41366
+ addonId: null,
41367
+ access: "create"
41368
+ },
41369
+ "keychainAdmin.listAudit": {
41370
+ capName: "keychain-admin",
41371
+ capScope: "system",
41372
+ addonId: null,
41373
+ access: "view"
41374
+ },
41375
+ "keychainAdmin.listSecrets": {
41376
+ capName: "keychain-admin",
41377
+ capScope: "system",
41378
+ addonId: null,
41379
+ access: "view"
41380
+ },
41381
+ "keychainAdmin.purgeRetiredKeys": {
41382
+ capName: "keychain-admin",
41383
+ capScope: "system",
41384
+ addonId: null,
41385
+ access: "delete"
41386
+ },
41387
+ "keychainAdmin.resetKeychain": {
41388
+ capName: "keychain-admin",
41389
+ capScope: "system",
41390
+ addonId: null,
41391
+ access: "delete"
41392
+ },
41393
+ "keychainAdmin.reveal": {
41394
+ capName: "keychain-admin",
41395
+ capScope: "system",
41396
+ addonId: null,
41397
+ access: "create"
41398
+ },
41399
+ "keychainAdmin.rotateMasterKey": {
41400
+ capName: "keychain-admin",
41401
+ capScope: "system",
41402
+ addonId: null,
41403
+ access: "create"
41404
+ },
41405
+ "keychainAdmin.status": {
41406
+ capName: "keychain-admin",
41407
+ capScope: "system",
41408
+ addonId: null,
41409
+ access: "view"
41410
+ },
39816
41411
  "lawnMowerControl.dock": {
39817
41412
  capName: "lawn-mower-control",
39818
41413
  capScope: "device",
@@ -41187,6 +42782,18 @@ Object.freeze({
41187
42782
  addonId: null,
41188
42783
  access: "create"
41189
42784
  },
42785
+ "pipelineAnalytics.addRetrainFrameFromRecording": {
42786
+ capName: "pipeline-analytics",
42787
+ capScope: "device",
42788
+ addonId: null,
42789
+ access: "create"
42790
+ },
42791
+ "pipelineAnalytics.addRetrainFramesToCollection": {
42792
+ capName: "pipeline-analytics",
42793
+ capScope: "device",
42794
+ addonId: null,
42795
+ access: "create"
42796
+ },
41190
42797
  "pipelineAnalytics.applyDeviceSettingsPatch": {
41191
42798
  capName: "pipeline-analytics",
41192
42799
  capScope: "device",
@@ -41235,12 +42842,24 @@ Object.freeze({
41235
42842
  addonId: null,
41236
42843
  access: "view"
41237
42844
  },
42845
+ "pipelineAnalytics.createRetrainCollection": {
42846
+ capName: "pipeline-analytics",
42847
+ capScope: "device",
42848
+ addonId: null,
42849
+ access: "create"
42850
+ },
41238
42851
  "pipelineAnalytics.deleteDeviceEvents": {
41239
42852
  capName: "pipeline-analytics",
41240
42853
  capScope: "device",
41241
42854
  addonId: null,
41242
42855
  access: "delete"
41243
42856
  },
42857
+ "pipelineAnalytics.deleteRetrainCollection": {
42858
+ capName: "pipeline-analytics",
42859
+ capScope: "device",
42860
+ addonId: null,
42861
+ access: "delete"
42862
+ },
41244
42863
  "pipelineAnalytics.deleteTracks": {
41245
42864
  capName: "pipeline-analytics",
41246
42865
  capScope: "device",
@@ -41445,6 +43064,24 @@ Object.freeze({
41445
43064
  addonId: null,
41446
43065
  access: "view"
41447
43066
  },
43067
+ "pipelineAnalytics.listRetrainAssistProfiles": {
43068
+ capName: "pipeline-analytics",
43069
+ capScope: "device",
43070
+ addonId: null,
43071
+ access: "view"
43072
+ },
43073
+ "pipelineAnalytics.listRetrainCollectionFrames": {
43074
+ capName: "pipeline-analytics",
43075
+ capScope: "device",
43076
+ addonId: null,
43077
+ access: "view"
43078
+ },
43079
+ "pipelineAnalytics.listRetrainCollections": {
43080
+ capName: "pipeline-analytics",
43081
+ capScope: "device",
43082
+ addonId: null,
43083
+ access: "view"
43084
+ },
41448
43085
  "pipelineAnalytics.listRetrainFrames": {
41449
43086
  capName: "pipeline-analytics",
41450
43087
  capScope: "device",
@@ -41493,6 +43130,12 @@ Object.freeze({
41493
43130
  addonId: null,
41494
43131
  access: "create"
41495
43132
  },
43133
+ "pipelineAnalytics.proposeRetrainAnnotationsWithLlm": {
43134
+ capName: "pipeline-analytics",
43135
+ capScope: "device",
43136
+ addonId: null,
43137
+ access: "create"
43138
+ },
41496
43139
  "pipelineAnalytics.pruneEvents": {
41497
43140
  capName: "pipeline-analytics",
41498
43141
  capScope: "device",
@@ -41547,6 +43190,18 @@ Object.freeze({
41547
43190
  addonId: null,
41548
43191
  access: "create"
41549
43192
  },
43193
+ "pipelineAnalytics.removeRetrainFrameFromCollection": {
43194
+ capName: "pipeline-analytics",
43195
+ capScope: "device",
43196
+ addonId: null,
43197
+ access: "delete"
43198
+ },
43199
+ "pipelineAnalytics.renameRetrainCollection": {
43200
+ capName: "pipeline-analytics",
43201
+ capScope: "device",
43202
+ addonId: null,
43203
+ access: "create"
43204
+ },
41550
43205
  "pipelineAnalytics.restageRetrainTrack": {
41551
43206
  capName: "pipeline-analytics",
41552
43207
  capScope: "device",
@@ -41835,6 +43490,12 @@ Object.freeze({
41835
43490
  addonId: null,
41836
43491
  access: "view"
41837
43492
  },
43493
+ "pipelineOrchestrator.abortNodeBenchmark": {
43494
+ capName: "pipeline-orchestrator",
43495
+ capScope: "system",
43496
+ addonId: null,
43497
+ access: "create"
43498
+ },
41838
43499
  "pipelineOrchestrator.applyAutoTune": {
41839
43500
  capName: "pipeline-orchestrator",
41840
43501
  capScope: "system",
@@ -41859,6 +43520,12 @@ Object.freeze({
41859
43520
  addonId: null,
41860
43521
  access: "create"
41861
43522
  },
43523
+ "pipelineOrchestrator.deleteNodeBenchmarkResult": {
43524
+ capName: "pipeline-orchestrator",
43525
+ capScope: "system",
43526
+ addonId: null,
43527
+ access: "delete"
43528
+ },
41862
43529
  "pipelineOrchestrator.getAgentLoad": {
41863
43530
  capName: "pipeline-orchestrator",
41864
43531
  capScope: "system",
@@ -41967,6 +43634,24 @@ Object.freeze({
41967
43634
  addonId: null,
41968
43635
  access: "view"
41969
43636
  },
43637
+ "pipelineOrchestrator.getNodeBenchmarkCandidates": {
43638
+ capName: "pipeline-orchestrator",
43639
+ capScope: "system",
43640
+ addonId: null,
43641
+ access: "view"
43642
+ },
43643
+ "pipelineOrchestrator.getNodeBenchmarkPreflight": {
43644
+ capName: "pipeline-orchestrator",
43645
+ capScope: "system",
43646
+ addonId: null,
43647
+ access: "view"
43648
+ },
43649
+ "pipelineOrchestrator.getNodeBenchmarkStatus": {
43650
+ capName: "pipeline-orchestrator",
43651
+ capScope: "system",
43652
+ addonId: null,
43653
+ access: "view"
43654
+ },
41970
43655
  "pipelineOrchestrator.getNodeInferenceDevices": {
41971
43656
  capName: "pipeline-orchestrator",
41972
43657
  capScope: "system",
@@ -42009,6 +43694,12 @@ Object.freeze({
42009
43694
  addonId: null,
42010
43695
  access: "view"
42011
43696
  },
43697
+ "pipelineOrchestrator.listNodeBenchmarkResults": {
43698
+ capName: "pipeline-orchestrator",
43699
+ capScope: "system",
43700
+ addonId: null,
43701
+ access: "view"
43702
+ },
42012
43703
  "pipelineOrchestrator.pauseForStorageMigration": {
42013
43704
  capName: "pipeline-orchestrator",
42014
43705
  capScope: "system",
@@ -42153,6 +43844,12 @@ Object.freeze({
42153
43844
  addonId: null,
42154
43845
  access: "create"
42155
43846
  },
43847
+ "pipelineOrchestrator.startNodeBenchmark": {
43848
+ capName: "pipeline-orchestrator",
43849
+ capScope: "system",
43850
+ addonId: null,
43851
+ access: "create"
43852
+ },
42156
43853
  "pipelineOrchestrator.suggestShares": {
42157
43854
  capName: "pipeline-orchestrator",
42158
43855
  capScope: "system",
@@ -42171,6 +43868,12 @@ Object.freeze({
42171
43868
  addonId: null,
42172
43869
  access: "create"
42173
43870
  },
43871
+ "pipelineRunner.abortNodeBench": {
43872
+ capName: "pipeline-runner",
43873
+ capScope: "system",
43874
+ addonId: null,
43875
+ access: "create"
43876
+ },
42174
43877
  "pipelineRunner.attachCamera": {
42175
43878
  capName: "pipeline-runner",
42176
43879
  capScope: "system",
@@ -42219,6 +43922,12 @@ Object.freeze({
42219
43922
  addonId: null,
42220
43923
  access: "view"
42221
43924
  },
43925
+ "pipelineRunner.getNodeBenchStatus": {
43926
+ capName: "pipeline-runner",
43927
+ capScope: "system",
43928
+ addonId: null,
43929
+ access: "view"
43930
+ },
42222
43931
  "pipelineRunner.getParkedTrackFrame": {
42223
43932
  capName: "pipeline-runner",
42224
43933
  capScope: "system",
@@ -42261,6 +43970,12 @@ Object.freeze({
42261
43970
  addonId: null,
42262
43971
  access: "create"
42263
43972
  },
43973
+ "pipelineRunner.startNodeBench": {
43974
+ capName: "pipeline-runner",
43975
+ capScope: "system",
43976
+ addonId: null,
43977
+ access: "create"
43978
+ },
42264
43979
  "plateGallery.assignPlate": {
42265
43980
  capName: "plate-gallery",
42266
43981
  capScope: "system",
@@ -47190,6 +48905,11 @@ Object.freeze({
47190
48905
  form: "single",
47191
48906
  optional: false
47192
48907
  }],
48908
+ "pipelineAnalytics.addRetrainFrameFromRecording": [{
48909
+ name: "deviceId",
48910
+ form: "single",
48911
+ optional: false
48912
+ }],
47193
48913
  "pipelineAnalytics.applyDeviceSettingsPatch": [{
47194
48914
  name: "deviceId",
47195
48915
  form: "single",
@@ -47380,6 +49100,11 @@ Object.freeze({
47380
49100
  form: "single",
47381
49101
  optional: false
47382
49102
  }],
49103
+ "pipelineAnalytics.proposeRetrainAnnotationsWithLlm": [{
49104
+ name: "deviceId",
49105
+ form: "single",
49106
+ optional: false
49107
+ }],
47383
49108
  "pipelineAnalytics.pruneEventsBefore": [{
47384
49109
  name: "deviceId",
47385
49110
  form: "single",
@@ -50791,10 +52516,12 @@ var BRIGHTNESS_CONTROLLER = {
50791
52516
  };
50792
52517
  /**
50793
52518
  * PTZ directional toggle — one `Alexa.ToggleController` instance per
50794
- * pan/tilt direction. Each is MOMENTARY: turning the toggle ON pulses
50795
- * a `continuousMove({direction})` for `ptzPulseMs` then `stop`, and
50796
- * the toggle auto-resets to OFF (handler returns the OFF state in
50797
- * `context.properties`). Alexa renders one row per instance in the
52519
+ * pan/tilt direction. Each is MOMENTARY: turning the toggle ON moves the
52520
+ * head ~1 s through the driver's own self-terminating `ptz.move` pulses
52521
+ * (`round(TARGET_TRAVEL_MS / getOptions().moveImpulseMs)` of them, one
52522
+ * when the impulse is unknown — see `handlers/ptz-travel.ts`; never
52523
+ * `continuousMove` / `stop`), and the toggle auto-resets to OFF (handler
52524
+ * returns the OFF state in `context.properties`). Alexa renders one row per instance in the
50798
52525
  * device detail; users can also say "Alexa, turn on Salone Pan Left".
50799
52526
  *
50800
52527
  * Instance ids match the directive routing in `ptz-direction.ts`.
@@ -52163,8 +53890,10 @@ var DetectionTracker = class {
52163
53890
  * (R4, D779).
52164
53891
  *
52165
53892
  * - `objectIdentifier`: "a UUID identifying a specific physical object" — one
52166
- * per pipeline track. A UUIDv5 of `<deviceId>:<trackId>` under this hub's
52167
- * namespace, so it is stable for the track's life and across restarts.
53893
+ * per pipeline track. Amazon types it "Version 4 UUID String", so it is
53894
+ * v4-shaped like the event id: a hash of `object|<deviceId>:<trackId>` under
53895
+ * this hub's namespace, stable for the track's life and across restarts.
53896
+ * (The first build sent a UUIDv5 here — D779 amendment.)
52168
53897
  * - `eventIdentifier`: Amazon requires a version-4 UUID per event in the
52169
53898
  * history. This skill posts ONE event per track, so the event id is derived
52170
53899
  * from the track too — a v4-shaped UUID whose 122 "random" bits are a
@@ -52191,10 +53920,6 @@ function stamp$1(bytes, version) {
52191
53920
  out[8] = (out[8] ?? 0) & 63 | 128;
52192
53921
  return out;
52193
53922
  }
52194
- /** RFC 4122 UUIDv5 (SHA-1, name-based). */
52195
- function uuidV5(namespace, name) {
52196
- return formatUuid(stamp$1(createHash("sha1").update(uuidBytes(namespace)).update(Buffer.from(name, "utf8")).digest(), 5));
52197
- }
52198
53923
  /** A version-4-formatted UUID whose bits are a hash — see the file header. */
52199
53924
  function hashedUuidV4(namespace, name) {
52200
53925
  return formatUuid(stamp$1(createHash("sha256").update(uuidBytes(namespace)).update(name, "utf8").digest(), 4));
@@ -52203,7 +53928,7 @@ function trackName(deviceId, trackId) {
52203
53928
  return `${deviceId}:${trackId}`;
52204
53929
  }
52205
53930
  function objectIdentifierFor(namespace, deviceId, trackId) {
52206
- return uuidV5(namespace, trackName(deviceId, trackId));
53931
+ return hashedUuidV4(namespace, `object|${trackName(deviceId, trackId)}`);
52207
53932
  }
52208
53933
  /** One event per track AND class: a track re-classified person → package is a
52209
53934
  * second event about the same object. */
@@ -52702,18 +54427,80 @@ async function handlePower(header, endpointRaw, deps, _callerIdentity) {
52702
54427
  };
52703
54428
  }
52704
54429
  //#endregion
52705
- //#region src/handlers/ptz-direction.ts
54430
+ //#region src/handlers/ptz-travel.ts
54431
+ /**
54432
+ * One Alexa PTZ voice command = ~1 s of head travel, built ONLY from the PTZ
54433
+ * cap's `ptz.move`.
54434
+ *
54435
+ * `move` is a self-terminating burst: the driver runs it for the pulse it
54436
+ * calibrated and stops the head itself, and reports that pulse as
54437
+ * `getOptions().moveImpulseMs` (ptz.cap.ts) — 200 reolink, 350 amcrest,
54438
+ * 500 hikvision, 1000 onvif. One pulse is too short a nudge for a voice
54439
+ * command, so the travel issues `round(TARGET_TRAVEL_MS / moveImpulseMs)` of
54440
+ * them back to back. There is no `continuousMove` / timer / `stop` here:
54441
+ * ONVIF does not implement `continuousMove`, and `move` needs no stop.
54442
+ *
54443
+ * Drivers differ in WHEN `move` resolves: hikvision awaits its own pulse,
54444
+ * reolink / amcrest / onvif return as soon as the burst has started. Each pulse
54445
+ * is awaited, then the remainder of its impulse is waited out, so both kinds
54446
+ * yield consecutive pulses rather than overlapping ones (which a driver would
54447
+ * collapse into one). An unknown impulse is a single `move`.
54448
+ */
54449
+ /** How far a voice command moves the head, in driver-pulse milliseconds. */
54450
+ var TARGET_TRAVEL_MS = 1e3;
54451
+ var realClock = {
54452
+ now: () => Date.now(),
54453
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
54454
+ };
54455
+ /** `max(1, round(TARGET_TRAVEL_MS / moveImpulseMs))`; unknown impulse → 1. */
54456
+ function ptzTravelPulses(moveImpulseMs) {
54457
+ if (moveImpulseMs === null || !(moveImpulseMs > 0)) return 1;
54458
+ return Math.max(1, Math.round(TARGET_TRAVEL_MS / moveImpulseMs));
54459
+ }
54460
+ /** `getOptions().moveImpulseMs`, or `null` when unreported or unreadable. */
54461
+ async function readImpulse(ptz, deviceId) {
54462
+ try {
54463
+ const ms = (await ptz.getOptions.query({ deviceId })).moveImpulseMs;
54464
+ return typeof ms === "number" && ms > 0 ? ms : null;
54465
+ } catch {
54466
+ return null;
54467
+ }
54468
+ }
52706
54469
  /**
52707
- * `Alexa.ToggleController/{TurnOn,TurnOff}` handler — restricted to PTZ
52708
- * directional instances (`ptz.panLeft` / `ptz.panRight` / `ptz.tiltUp`
52709
- * / `ptz.tiltDown`). Mirrors the HAP momentary-switch pattern: every
52710
- * `TurnOn` pulses a `continuousMove({pan|tilt: ±1})` and schedules a
52711
- * `stop` after `ptzPulseMs`. The handler returns immediately with
52712
- * `toggleState=OFF` so Alexa reflects the momentary nature in its UI.
52713
- *
52714
- * `TurnOff` is a no-op (success response) — Alexa never delivers it in
52715
- * normal momentary usage, but accept it for resilience.
54470
+ * Runs the travel. REJECTS when the first `move` fails (nothing moved, the
54471
+ * caller answers with an error); a later failure stops the travel and is
54472
+ * returned in `error`, since the head has already moved.
52716
54473
  */
54474
+ async function runPtzTravel(ptz, deviceId, direction, clock = realClock) {
54475
+ const impulse = await readImpulse(ptz, deviceId);
54476
+ const pulses = ptzTravelPulses(impulse);
54477
+ for (let i = 0; i < pulses; i++) {
54478
+ const startedAt = clock.now();
54479
+ try {
54480
+ await ptz.move.mutate({
54481
+ deviceId,
54482
+ ...direction
54483
+ });
54484
+ } catch (err) {
54485
+ if (i === 0) throw err;
54486
+ return {
54487
+ pulses,
54488
+ issued: i,
54489
+ error: err instanceof Error ? err : new Error(String(err))
54490
+ };
54491
+ }
54492
+ if (!(i === pulses - 1) && impulse !== null) {
54493
+ const remaining = impulse - (clock.now() - startedAt);
54494
+ if (remaining > 0) await clock.sleep(remaining);
54495
+ }
54496
+ }
54497
+ return {
54498
+ pulses,
54499
+ issued: pulses
54500
+ };
54501
+ }
54502
+ //#endregion
54503
+ //#region src/handlers/ptz-direction.ts
52717
54504
  var ToggleDirectiveSchema = object({
52718
54505
  header: object({
52719
54506
  namespace: literal("Alexa.ToggleController"),
@@ -52747,27 +54534,24 @@ async function handlePtzDirection(header, endpointRaw, deps, _callerIdentity) {
52747
54534
  const move = INSTANCE_TO_MOVE[instance];
52748
54535
  if (!move) return errorEnvelope(header, "INVALID_DIRECTIVE", `ToggleController instance not supported: ${instance}`);
52749
54536
  const api = deps.ctx.api;
54537
+ const deviceId = parsedId.deviceId;
52750
54538
  try {
52751
- await api.ptz.continuousMove.mutate({
52752
- deviceId: parsedId.deviceId,
52753
- ...move
52754
- });
52755
- setTimeout(() => {
52756
- api.ptz.stop.mutate({ deviceId: parsedId.deviceId }).catch((err) => {
52757
- deps.logger.debug("alexa ptz-direction: stop after pulse failed (non-fatal)", { meta: {
52758
- deviceId: parsedId.deviceId,
52759
- instance,
52760
- error: err instanceof Error ? err.message : String(err)
52761
- } });
52762
- });
52763
- }, deps.ptzPulseMs);
54539
+ const travel = await runPtzTravel(api.ptz, deviceId, move);
54540
+ if (travel.error) deps.logger.warn("alexa ptz-direction: ptz.move failed mid-travel", { meta: {
54541
+ deviceId,
54542
+ instance,
54543
+ issued: travel.issued,
54544
+ pulses: travel.pulses,
54545
+ error: travel.error.message
54546
+ } });
52764
54547
  } catch (err) {
52765
- deps.logger.warn("alexa ptz-direction: continuousMove failed", { meta: {
52766
- deviceId: parsedId.deviceId,
54548
+ const message = err instanceof Error ? err.message : String(err);
54549
+ deps.logger.warn("alexa ptz-direction: ptz.move failed", { meta: {
54550
+ deviceId,
52767
54551
  instance,
52768
- error: err instanceof Error ? err.message : String(err)
54552
+ error: message
52769
54553
  } });
52770
- return errorEnvelope(header, "INTERNAL_ERROR", `ptz.continuousMove failed: ${err instanceof Error ? err.message : String(err)}`);
54554
+ return errorEnvelope(header, "INTERNAL_ERROR", `ptz.move failed: ${message}`);
52771
54555
  }
52772
54556
  return successResponse(header, endpointId, "OFF", instance);
52773
54557
  }
@@ -52796,18 +54580,6 @@ function successResponse(header, endpointId, toggleState, instance) {
52796
54580
  }
52797
54581
  //#endregion
52798
54582
  //#region src/handlers/ptz-preset.ts
52799
- /**
52800
- * `Alexa.ModeController/SetMode` handler — dispatches three PTZ instances:
52801
- *
52802
- * `ptz.preset` — preset-recall. Decodes the preset name from the mode
52803
- * value and calls `ptz.goToPreset({deviceId, presetId})`.
52804
- *
52805
- * `ptz.pan` — momentary pan. `ptz.pan.left|right|center` issues a
52806
- * continuousMove pulse stopped on a timer; the dropdown reverts to
52807
- * `center` server-side so subsequent picks re-trigger the move.
52808
- *
52809
- * `ptz.tilt` — momentary tilt. Mirrors ptz.pan with up|down|center.
52810
- */
52811
54583
  var SetModeDirectiveSchema = object({
52812
54584
  header: object({
52813
54585
  namespace: literal("Alexa.ModeController"),
@@ -52818,7 +54590,6 @@ var SetModeDirectiveSchema = object({
52818
54590
  payload: SetModePayloadSchema
52819
54591
  });
52820
54592
  var PTZ_PRESET_MODE_PREFIX = "ptz.preset.";
52821
- var PTZ_DIRECTION_PULSE_MS = 1e3;
52822
54593
  var PTZ_PAN_MOVES = {
52823
54594
  "ptz.pan.left": { pan: -1 },
52824
54595
  "ptz.pan.right": { pan: 1 },
@@ -52850,29 +54621,28 @@ async function handleSetMode(header, endpointRaw, payloadRaw, deps, _callerIdent
52850
54621
  if (instance === "ptz.pan" || instance === "ptz.tilt") {
52851
54622
  const move = (instance === "ptz.pan" ? PTZ_PAN_MOVES : PTZ_TILT_MOVES)[mode];
52852
54623
  if (!move) return errorEnvelope(header, "INVALID_DIRECTIVE", `mode '${mode}' is not a valid ${instance} mode`);
52853
- if (Object.keys(move).length === 0) await api.ptz.stop.mutate({ deviceId: parsedId.deviceId }).catch((err) => {
52854
- deps.logger.debug("alexa ptz-mode: stop (center) failed (non-fatal)", { meta: {
52855
- deviceId: parsedId.deviceId,
52856
- instance,
52857
- error: err instanceof Error ? err.message : String(err)
52858
- } });
52859
- });
52860
- else try {
52861
- await api.ptz.continuousMove.mutate({
52862
- deviceId: parsedId.deviceId,
52863
- ...move
52864
- });
52865
- setTimeout(() => {
52866
- api.ptz.stop.mutate({ deviceId: parsedId.deviceId }).catch((err) => {
52867
- deps.logger.debug("alexa ptz-mode: stop after pulse failed (non-fatal)", { meta: {
52868
- deviceId: parsedId.deviceId,
52869
- instance,
52870
- error: err instanceof Error ? err.message : String(err)
52871
- } });
52872
- });
52873
- }, PTZ_DIRECTION_PULSE_MS);
52874
- } catch (err) {
52875
- return errorEnvelope(header, "INTERNAL_ERROR", `ptz.continuousMove failed: ${err instanceof Error ? err.message : String(err)}`);
54624
+ if (Object.keys(move).length > 0) {
54625
+ const deviceId = parsedId.deviceId;
54626
+ try {
54627
+ const travel = await runPtzTravel(api.ptz, deviceId, move);
54628
+ if (travel.error) deps.logger.warn("alexa ptz-mode: ptz.move failed mid-travel", { meta: {
54629
+ deviceId,
54630
+ instance,
54631
+ mode,
54632
+ issued: travel.issued,
54633
+ pulses: travel.pulses,
54634
+ error: travel.error.message
54635
+ } });
54636
+ } catch (err) {
54637
+ const message = err instanceof Error ? err.message : String(err);
54638
+ deps.logger.warn("alexa ptz-mode: ptz.move failed", { meta: {
54639
+ deviceId,
54640
+ instance,
54641
+ mode,
54642
+ error: message
54643
+ } });
54644
+ return errorEnvelope(header, "INTERNAL_ERROR", `ptz.move failed: ${message}`);
54645
+ }
52876
54646
  }
52877
54647
  return modeSuccessResponse(header, endpointId, instance, instance === "ptz.pan" ? "ptz.pan.center" : "ptz.tilt.center");
52878
54648
  }
@@ -53174,6 +54944,7 @@ async function handleReportState(header, endpointRaw, deps, _callerIdentity) {
53174
54944
  });
53175
54945
  }
53176
54946
  } catch (err) {
54947
+ if (err instanceof ChildUnreachableError) return errorEnvelope(header, "ENDPOINT_UNREACHABLE", err.message);
53177
54948
  deps.logger.warn("alexa report-state: collection failed", { meta: {
53178
54949
  endpointId,
53179
54950
  error: err instanceof Error ? err.message : String(err)
@@ -53321,16 +55092,35 @@ async function collectAutotrackProperties(api, parentDeviceId, properties, timeO
53321
55092
  uncertaintyInMilliseconds: 0
53322
55093
  });
53323
55094
  }
55095
+ /** The child's `switch` cannot be read: answered `ENDPOINT_UNREACHABLE`. */
55096
+ var ChildUnreachableError = class extends Error {};
55097
+ /** One warning per child per window (a sleeping battery camera's accessories
55098
+ * are polled by Alexa for as long as it sleeps). */
55099
+ var CHILD_WARN_EVERY_MS = 10 * 6e4;
55100
+ var childWarnedAt = /* @__PURE__ */ new Map();
55101
+ function warnChildUnreadable(logger, childDeviceId, error) {
55102
+ const now = Date.now();
55103
+ const last = childWarnedAt.get(childDeviceId);
55104
+ if (last !== void 0 && now - last < CHILD_WARN_EVERY_MS) return;
55105
+ childWarnedAt.set(childDeviceId, now);
55106
+ logger.warn("alexa report-state: child accessory switch unreadable — answered ENDPOINT_UNREACHABLE", { meta: {
55107
+ childDeviceId,
55108
+ error,
55109
+ repeatsSuppressedForMs: CHILD_WARN_EVERY_MS
55110
+ } });
55111
+ }
53324
55112
  async function collectChildProperties(api, childDeviceId, properties, timeOfSample, logger) {
55113
+ let failure = null;
53325
55114
  const switchState = await api.switch.getStatus.query({ deviceId: childDeviceId }).catch((err) => {
53326
- logger.warn("alexa report-state: switch.getStatus FAILED — Alexa will show endpoint as \"unavailable\"", { meta: {
53327
- childDeviceId,
53328
- error: err instanceof Error ? err.message : String(err)
53329
- } });
55115
+ failure = err instanceof Error ? err.message : String(err);
53330
55116
  return null;
53331
55117
  });
53332
- if (switchState === null || switchState === void 0) logger.warn("alexa report-state: switch.getStatus returned no payload — surfacing as unavailable", { meta: { childDeviceId } });
53333
- else logger.info("alexa report-state: switch.getStatus payload", { meta: {
55118
+ if (switchState === null || switchState === void 0) {
55119
+ warnChildUnreadable(logger, childDeviceId, failure ?? "no payload");
55120
+ throw new ChildUnreachableError(`accessory ${childDeviceId} cannot be read right now`);
55121
+ }
55122
+ childWarnedAt.delete(childDeviceId);
55123
+ logger.debug("alexa report-state: switch.getStatus payload", { meta: {
53334
55124
  childDeviceId,
53335
55125
  payload: switchState
53336
55126
  } });
@@ -54151,44 +55941,84 @@ function refuse(reply, status, error) {
54151
55941
  reply.code(status).type("application/json").header("cache-control", "no-store").header("content-encoding", "identity");
54152
55942
  reply.send({ error });
54153
55943
  }
55944
+ function headerOf(headers, name) {
55945
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === name) return value;
55946
+ }
55947
+ /**
55948
+ * Exactly one line per fetch, whatever the answer (D779 amendment): "did Amazon ever
55949
+ * fetch the image, and what did it get?" is otherwise unanswerable — a served
55950
+ * image used to log nothing. Never the signature, never the token.
55951
+ */
55952
+ function logFetch(deps, req, started, outcome) {
55953
+ const level = outcome.status === 401 || outcome.status === 403 ? "warn" : "info";
55954
+ deps.logger[level]("alexa snapshot: fetch answered", {
55955
+ ...outcome.deviceId !== void 0 ? { tags: { deviceId: outcome.deviceId } } : {},
55956
+ meta: {
55957
+ status: outcome.status,
55958
+ ...outcome.imageId !== void 0 ? { imageId: outcome.imageId } : {},
55959
+ ...outcome.reason !== void 0 ? { reason: outcome.reason } : {},
55960
+ ...outcome.lazy !== void 0 ? { lazy: outcome.lazy } : {},
55961
+ ...outcome.bytes !== void 0 ? { bytes: outcome.bytes } : {},
55962
+ tookMs: Date.now() - started,
55963
+ userAgent: headerOf(req.headers, "user-agent") ?? null,
55964
+ ...outcome.extra
55965
+ }
55966
+ });
55967
+ }
54154
55968
  async function handleSnapshotFetch(req, reply, deps) {
55969
+ const started = Date.now();
54155
55970
  if (!deps.enabled()) {
55971
+ logFetch(deps, req, started, {
55972
+ status: 410,
55973
+ reason: "switched-off"
55974
+ });
54156
55975
  refuse(reply, 410, "snapshots are switched off");
54157
55976
  return;
54158
55977
  }
54159
55978
  const imageId = IMAGE_FILE.exec(req.params["file"] ?? "")?.[1];
54160
55979
  if (imageId === void 0) {
55980
+ logFetch(deps, req, started, {
55981
+ status: 410,
55982
+ reason: "not-a-snapshot-address"
55983
+ });
54161
55984
  refuse(reply, 410, "not a snapshot address");
54162
55985
  return;
54163
55986
  }
54164
55987
  const found = deps.store.lookup(imageId, req.query["exp"], req.query["sig"]);
54165
55988
  if (found.kind === "gone") {
54166
- deps.logger.debug("alexa snapshot: fetch of a gone image", { meta: {
55989
+ logFetch(deps, req, started, {
55990
+ status: 410,
54167
55991
  imageId,
54168
55992
  reason: found.reason
54169
- } });
55993
+ });
54170
55994
  refuse(reply, 410, found.reason);
54171
55995
  return;
54172
55996
  }
54173
55997
  const image = found.image;
55998
+ const lazy = image.load !== void 0 || image.bytes.byteLength === 0;
55999
+ const base = {
56000
+ imageId,
56001
+ deviceId: image.deviceId,
56002
+ lazy
56003
+ };
54174
56004
  const token = bearerOf(req.headers);
54175
56005
  const verified = token === null ? null : await deps.verifyToken(token).catch(() => null);
54176
56006
  if (verified === null) {
54177
- deps.logger.warn("alexa snapshot: refused a fetch without a valid account-link token", {
54178
- tags: { deviceId: image.deviceId },
54179
- meta: {
54180
- imageId,
54181
- hadToken: token !== null
54182
- }
56007
+ logFetch(deps, req, started, {
56008
+ ...base,
56009
+ status: 401,
56010
+ reason: "no valid account-link token",
56011
+ extra: { hadToken: token !== null }
54183
56012
  });
54184
56013
  refuse(reply, 401, "a valid Alexa account-link token is required");
54185
56014
  return;
54186
56015
  }
54187
56016
  if (verified.userId !== image.userId || !await deps.mayView(verified.userId, image.deviceId)) {
54188
- deps.logger.warn("alexa snapshot: refused — the token does not belong to the user this image was made for, or that user lost the camera", {
54189
- tags: { deviceId: image.deviceId },
54190
- meta: {
54191
- imageId,
56017
+ logFetch(deps, req, started, {
56018
+ ...base,
56019
+ status: 403,
56020
+ reason: "token of another user, or the user lost the camera",
56021
+ extra: {
54192
56022
  tokenUser: verified.userId,
54193
56023
  imageUser: image.userId
54194
56024
  }
@@ -54198,13 +56028,19 @@ async function handleSnapshotFetch(req, reply, deps) {
54198
56028
  }
54199
56029
  const bytes = await deps.store.bytesOf(image);
54200
56030
  if (bytes === null || bytes.byteLength === 0) {
54201
- deps.logger.debug("alexa snapshot: no image to serve for this event", {
54202
- tags: { deviceId: image.deviceId },
54203
- meta: { imageId }
56031
+ logFetch(deps, req, started, {
56032
+ ...base,
56033
+ status: 410,
56034
+ reason: "no image for this event"
54204
56035
  });
54205
56036
  refuse(reply, 410, "no image is available for this event");
54206
56037
  return;
54207
56038
  }
56039
+ logFetch(deps, req, started, {
56040
+ ...base,
56041
+ status: 200,
56042
+ bytes: bytes.byteLength
56043
+ });
54208
56044
  reply.code(200).type(image.contentType).header("cache-control", "private, no-store").header("content-encoding", "identity").header("content-length", String(bytes.byteLength));
54209
56045
  reply.send(bytes);
54210
56046
  }
@@ -54687,6 +56523,8 @@ var LwaEventGateway = class {
54687
56523
  /** Per-endpoint count of `ObjectDetection` events dropped because the
54688
56524
  * endpoint does not declare {@link SMART_VISION_INTERFACE}. */
54689
56525
  smartVisionDropped = /* @__PURE__ */ new Map();
56526
+ /** Endpoints whose redacted ObjectDetection body was logged (D779 amendment). */
56527
+ smartVisionSampled = /* @__PURE__ */ new Set();
54690
56528
  /** Latches the "no grant at all" warning so it's logged ONCE, not on every
54691
56529
  * doorbell/motion event while no account has a push grant. */
54692
56530
  warnedNoGrant = false;
@@ -54739,6 +56577,7 @@ var LwaEventGateway = class {
54739
56577
  this.lastSentAt.clear();
54740
56578
  this.motionHold.clear();
54741
56579
  this.smartVisionDropped.clear();
56580
+ this.smartVisionSampled.clear();
54742
56581
  this.warnedNoToken.clear();
54743
56582
  this.warnedLegacyWaiting.clear();
54744
56583
  this.migrationRetryAt.clear();
@@ -54943,6 +56782,24 @@ var LwaEventGateway = class {
54943
56782
  for (const account of linked) {
54944
56783
  if (!this.mayPush(account.userId, opts.deviceId, "object detection")) continue;
54945
56784
  const images = opts.imagesFor?.(account.userId) ?? {};
56785
+ if (!this.smartVisionSampled.has(endpointId)) {
56786
+ this.smartVisionSampled.add(endpointId);
56787
+ this.deps.logger.info("alexa lwa-gateway: ObjectDetection body sample (redacted)", {
56788
+ tags: { deviceId: opts.deviceId },
56789
+ meta: { body: redactedObjectDetectionSample({
56790
+ userId: account.userId,
56791
+ endpointId,
56792
+ detections: [{
56793
+ eventIdentifier: d.eventIdentifier,
56794
+ imageNetClass: d.imageNetClass,
56795
+ objectIdentifier: d.objectIdentifier,
56796
+ ...images
56797
+ }],
56798
+ timestamp: d.timestamp,
56799
+ accessToken: ""
56800
+ }) }
56801
+ });
56802
+ }
54946
56803
  await this.postEventBody(account.userId, endpointId, (accessToken) => buildSmartVisionObjectDetectionEvent({
54947
56804
  userId: account.userId,
54948
56805
  endpointId,
@@ -55493,6 +57350,29 @@ function buildSmartVisionObjectDetectionEvent(opts) {
55493
57350
  })) }
55494
57351
  } };
55495
57352
  }
57353
+ /** Every `sig=` query value in a URI, for a log line. */
57354
+ function redactSignature(uri) {
57355
+ return uri?.replace(/([?&]sig=)[^&]*/g, "$1<redacted>");
57356
+ }
57357
+ /**
57358
+ * The ObjectDetection body exactly as built for the wire, minus its secrets
57359
+ * (D779 amendment): the scope token and every image URI's signature. Logged once per
57360
+ * endpoint per process so the shape can be compared with Amazon's example.
57361
+ */
57362
+ function redactedObjectDetectionSample(opts) {
57363
+ const body = buildSmartVisionObjectDetectionEvent({
57364
+ ...opts,
57365
+ accessToken: "<redacted>"
57366
+ });
57367
+ return { event: {
57368
+ ...body.event,
57369
+ payload: { events: body.event.payload.events.map((e) => ({
57370
+ ...e,
57371
+ ...e.frameImageUri !== void 0 ? { frameImageUri: redactSignature(e.frameImageUri) } : {},
57372
+ ...e.croppedImageUri !== void 0 ? { croppedImageUri: redactSignature(e.croppedImageUri) } : {}
57373
+ })) }
57374
+ } };
57375
+ }
55496
57376
  function buildAddOrUpdateReportPayload(_userId, endpoints, accessToken) {
55497
57377
  return { event: {
55498
57378
  header: {
@@ -55565,6 +57445,11 @@ async function removeEndpointsFromAlexa(deps) {
55565
57445
  //#endregion
55566
57446
  //#region src/settings-schema.ts
55567
57447
  var ADDON_ID = "export-alexa";
57448
+ /** The rows that exist only for a custom skill. */
57449
+ var CUSTOM_SKILL_ONLY = {
57450
+ field: "skillMode",
57451
+ equals: "custom"
57452
+ };
55568
57453
  var CONNECTION_DESCRIPTION = [
55569
57454
  "Public skill (default): enable the \"CamStack\" skill in the Alexa app and link your account. No Amazon developer setup is needed.",
55570
57455
  "Custom skill: enter your skill id and deploy the skill Lambda from this addon's lambda-template in your own AWS account.",
@@ -55614,13 +57499,14 @@ function connectionSection(input) {
55614
57499
  ],
55615
57500
  default: defaults.region
55616
57501
  },
55617
- ...input.skillMode === "custom" ? [{
57502
+ {
55618
57503
  type: "text",
55619
57504
  key: "expectedAudience",
55620
57505
  label: "Alexa skill id",
55621
57506
  description: "Your skill's id, format amzn1.ask.skill.xxxx, from the Alexa developer console. Directives for any other skill are refused.",
55622
- default: defaults.expectedAudience
55623
- }] : [],
57507
+ default: defaults.expectedAudience,
57508
+ showWhen: CUSTOM_SKILL_ONLY
57509
+ },
55624
57510
  {
55625
57511
  type: "addon-action-select",
55626
57512
  key: "publicHubUrl",
@@ -55763,6 +57649,7 @@ function customSkillSecuritySection(input) {
55763
57649
  key: "__ingress-token-info",
55764
57650
  label: "Ingress token",
55765
57651
  variant: "info",
57652
+ showWhen: CUSTOM_SKILL_ONLY,
55766
57653
  content: "An optional shared secret your skill Lambda sends on every call to this hub. Show it, put it in the Lambda's HUB_INGRESS_TOKEN environment variable, then turn on \"Require ingress token\". After a rotation, update the Lambda."
55767
57654
  },
55768
57655
  {
@@ -55774,7 +57661,8 @@ function customSkillSecuritySection(input) {
55774
57661
  buttonLabel: "Show ingress token",
55775
57662
  action: "revealIngressToken",
55776
57663
  variant: "default",
55777
- successMessage: "Ingress token: {ingressToken}"
57664
+ successMessage: "Ingress token: {ingressToken}",
57665
+ showWhen: CUSTOM_SKILL_ONLY
55778
57666
  },
55779
57667
  {
55780
57668
  type: "addon-action-button",
@@ -55786,14 +57674,16 @@ function customSkillSecuritySection(input) {
55786
57674
  action: "regenerateIngressToken",
55787
57675
  variant: "default",
55788
57676
  successMessage: "New ingress token: {ingressToken} — put it in HUB_INGRESS_TOKEN now.",
55789
- confirmMessage: "Rotate the ingress token? With \"Require ingress token\" on, the Lambda is refused until you update HUB_INGRESS_TOKEN."
57677
+ confirmMessage: "Rotate the ingress token? With \"Require ingress token\" on, the Lambda is refused until you update HUB_INGRESS_TOKEN.",
57678
+ showWhen: CUSTOM_SKILL_ONLY
55790
57679
  },
55791
57680
  {
55792
57681
  type: "boolean",
55793
57682
  key: "enforceIngressToken",
55794
57683
  label: "Require ingress token",
55795
57684
  description: "When on, this hub refuses skill calls without the matching token. Turn it on only after HUB_INGRESS_TOKEN is set in your Lambda.",
55796
- default: input.defaults.enforceIngressToken
57685
+ default: input.defaults.enforceIngressToken,
57686
+ showWhen: CUSTOM_SKILL_ONLY
55797
57687
  }
55798
57688
  ]
55799
57689
  };
@@ -55804,7 +57694,7 @@ function buildAlexaSettingsSections(input) {
55804
57694
  featuresSection(input),
55805
57695
  accountLinkSection(input),
55806
57696
  troubleshootingSection(),
55807
- ...input.skillMode === "custom" ? [customSkillSecuritySection(input)] : []
57697
+ customSkillSecuritySection(input)
55808
57698
  ];
55809
57699
  }
55810
57700
  //#endregion
@@ -56184,6 +58074,8 @@ function sensorSelectionField(input) {
56184
58074
  immediate: true
56185
58075
  };
56186
58076
  }
58077
+ /** Minimum spacing of two events of one class on one camera (D779 amendment). */
58078
+ var CAMERA_EVENT_MIN_INTERVAL_MS = 3e4;
56187
58079
  /** Bound on the per-track bookkeeping should `end` events be lost (D8). */
56188
58080
  var MAX_TRACKED = 5e3;
56189
58081
  var TrackLifecycleSchema = object({
@@ -56206,6 +58098,9 @@ var ObjectDetectionFeed = class {
56206
58098
  handled = /* @__PURE__ */ new Map();
56207
58099
  /** objectIdentifier → last event time. */
56208
58100
  lastPosted = /* @__PURE__ */ new Map();
58101
+ /** `${deviceId}|${class}` → last event time, and tracks held back since. */
58102
+ lastCameraPost = /* @__PURE__ */ new Map();
58103
+ heldBack = /* @__PURE__ */ new Map();
56209
58104
  off = null;
56210
58105
  constructor(deps) {
56211
58106
  this.deps = deps;
@@ -56230,6 +58125,8 @@ var ObjectDetectionFeed = class {
56230
58125
  this.off = null;
56231
58126
  this.handled.clear();
56232
58127
  this.lastPosted.clear();
58128
+ this.lastCameraPost.clear();
58129
+ this.heldBack.clear();
56233
58130
  }
56234
58131
  /** Per-track entries currently held (tests, diagnostics). */
56235
58132
  trackedCount() {
@@ -56250,6 +58147,12 @@ var ObjectDetectionFeed = class {
56250
58147
  const objectIdentifier = objectIdentifierFor(namespace, event.deviceId, event.trackId);
56251
58148
  const last = this.lastPosted.get(objectIdentifier);
56252
58149
  if (last !== void 0 && now - last < 3e4) return;
58150
+ const cameraKey = `${event.deviceId}|${cls}`;
58151
+ const lastOnCamera = this.lastCameraPost.get(cameraKey);
58152
+ if (lastOnCamera !== void 0 && now - lastOnCamera < 3e4) {
58153
+ if (event.phase === "start") this.heldBack.set(cameraKey, (this.heldBack.get(cameraKey) ?? 0) + 1);
58154
+ return;
58155
+ }
56253
58156
  this.mark(key, now);
56254
58157
  if (!(await this.deps.reportableClasses(event.deviceId)).includes(cls)) return;
56255
58158
  const detection = {
@@ -56260,7 +58163,20 @@ var ObjectDetectionFeed = class {
56260
58163
  timestamp: now
56261
58164
  };
56262
58165
  this.lastPosted.set(objectIdentifier, now);
58166
+ this.lastCameraPost.set(cameraKey, now);
56263
58167
  this.pruneLastPosted(now);
58168
+ const held = this.heldBack.get(cameraKey) ?? 0;
58169
+ if (held > 0) {
58170
+ this.heldBack.delete(cameraKey);
58171
+ this.deps.logger.info("alexa object detection: tracks held back by the per-camera window", {
58172
+ tags: { deviceId: event.deviceId },
58173
+ meta: {
58174
+ imageNetClass: cls,
58175
+ heldBack: held,
58176
+ windowMs: CAMERA_EVENT_MIN_INTERVAL_MS
58177
+ }
58178
+ });
58179
+ }
56264
58180
  await this.deps.remember(event.deviceId, {
56265
58181
  e: detection.eventIdentifier,
56266
58182
  t: event.trackId,
@@ -56679,7 +58595,15 @@ function verifyExpiringUrl(input) {
56679
58595
  */
56680
58596
  /** How long a minted image URL is valid — Amazon recommends ten minutes. */
56681
58597
  var SNAPSHOT_URL_TTL_MS = 10 * 6e4;
58598
+ /** Bound on entries holding pixels (GetSnapshot images, loaded lazy ones). */
56682
58599
  var MAX_IMAGES = 128;
58600
+ /**
58601
+ * Bound on lazy entries not yet fetched (D779 amendment). They hold no pixels — a closure
58602
+ * over (device, track) — so the bound only guards against a runaway. It must
58603
+ * cover ten minutes of events: at the measured ~15 events/min, two URIs each,
58604
+ * the shared 128 bound evicted a URI ~4 min after Alexa was handed it.
58605
+ */
58606
+ var MAX_LAZY_IMAGES = 4096;
56683
58607
  var MAX_TOTAL_BYTES = 24 * 1024 * 1024;
56684
58608
  var SnapshotStore = class {
56685
58609
  secret;
@@ -56809,9 +58733,18 @@ var SnapshotStore = class {
56809
58733
  for (const [id, image] of this.images) if (image.expiresAt <= now) this.drop(id, image);
56810
58734
  }
56811
58735
  evictOverBounds() {
58736
+ let lazy = 0;
58737
+ for (const image of this.images.values()) if (image.load !== void 0) lazy += 1;
58738
+ let loaded = this.images.size - lazy;
56812
58739
  for (const [id, image] of this.images) {
56813
- if (this.images.size <= MAX_IMAGES && this.totalBytes <= MAX_TOTAL_BYTES) return;
58740
+ const overLoaded = loaded > MAX_IMAGES || this.totalBytes > MAX_TOTAL_BYTES;
58741
+ const overLazy = lazy > MAX_LAZY_IMAGES;
58742
+ if (!overLoaded && !overLazy) return;
58743
+ const isLazy = image.load !== void 0;
58744
+ if (isLazy ? !overLazy : !overLoaded) continue;
56814
58745
  this.drop(id, image);
58746
+ if (isLazy) lazy -= 1;
58747
+ else loaded -= 1;
56815
58748
  }
56816
58749
  }
56817
58750
  drop(id, image) {
@@ -57541,6 +59474,14 @@ var SUPPORTED_KINDS = [
57541
59474
  DeviceType.Generic
57542
59475
  ];
57543
59476
  var ExportAlexaAddon = class extends BaseAddon {
59477
+ /** Hidden state that is a credential — kept in the keychain, never the blob (D787). */
59478
+ secretStatePaths = [
59479
+ "ingressToken",
59480
+ "snapshotSigningKey",
59481
+ "tokens[userId].accessToken",
59482
+ "tokens[userId].refreshToken",
59483
+ "tokens[userId].brokerCredential"
59484
+ ];
57544
59485
  lwaGateway = null;
57545
59486
  syncEngine = null;
57546
59487
  /** S1 (D767): the linking user's live grants. Built in `onInitialize`, the
@@ -57568,7 +59509,7 @@ var ExportAlexaAddon = class extends BaseAddon {
57568
59509
  if (!this.config.snapshotSigningKey) await this.updateGlobalSettings({ snapshotSigningKey: generateIngressToken() });
57569
59510
  if (!this.config.visionNamespace) await this.updateGlobalSettings({ visionNamespace: randomUUID() });
57570
59511
  if (this.config.lwaClientSecret) {
57571
- await this.updateGlobalSettings({ lwaClientSecret: "" });
59512
+ await this.clearSecrets(["lwaClientSecret"]);
57572
59513
  this.ctx.logger.warn("export-alexa: removed a stored LWA client secret — the hub no longer uses one; your skill Lambda holds it (D753)");
57573
59514
  }
57574
59515
  await this.backfillLinkedAccounts();
@@ -57643,8 +59584,7 @@ var ExportAlexaAddon = class extends BaseAddon {
57643
59584
  },
57644
59585
  ptzDirection: {
57645
59586
  ctx: this.ctx,
57646
- logger: this.ctx.logger,
57647
- ptzPulseMs: 1e3
59587
+ logger: this.ctx.logger
57648
59588
  },
57649
59589
  powerController: {
57650
59590
  ctx: this.ctx,
@@ -58396,7 +60336,6 @@ var ExportAlexaAddon = class extends BaseAddon {
58396
60336
  * linking rows live in the `device-export` setup block instead. */
58397
60337
  globalSettingsSchema() {
58398
60338
  return { sections: buildAlexaSettingsSections({
58399
- skillMode: this.config.skillMode,
58400
60339
  linkedAccounts: this.config.linkedAccounts,
58401
60340
  tokens: this.config.tokens,
58402
60341
  now: Date.now(),