@camstack/addon-provider-reolink 1.2.222 → 1.2.224

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.
package/dist/addon.js CHANGED
@@ -105254,6 +105254,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
105254
105254
  var booleanProcessor = (_schema, _ctx, json, _params) => {
105255
105255
  json.type = "boolean";
105256
105256
  };
105257
+ var bigintProcessor = (_schema, ctx, _json, _params) => {
105258
+ if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
105259
+ };
105260
+ var symbolProcessor = (_schema, ctx, _json, _params) => {
105261
+ if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
105262
+ };
105257
105263
  var nullProcessor = (_schema, ctx, json, _params) => {
105258
105264
  if (ctx.target === "openapi-3.0") {
105259
105265
  json.type = "string";
@@ -105261,12 +105267,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
105261
105267
  json.enum = [null];
105262
105268
  } else json.type = "null";
105263
105269
  };
105270
+ var undefinedProcessor = (_schema, ctx, _json, _params) => {
105271
+ if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
105272
+ };
105264
105273
  var voidProcessor = (_schema, ctx, _json, _params) => {
105265
105274
  if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
105266
105275
  };
105267
105276
  var neverProcessor = (_schema, _ctx, json, _params) => {
105268
105277
  json.not = {};
105269
105278
  };
105279
+ var anyProcessor = (_schema, _ctx, _json, _params) => {};
105280
+ var unknownProcessor = (_schema, _ctx, _json, _params) => {};
105270
105281
  var dateProcessor = (_schema, ctx, _json, _params) => {
105271
105282
  if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
105272
105283
  };
@@ -105298,12 +105309,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
105298
105309
  json.enum = vals;
105299
105310
  }
105300
105311
  };
105312
+ var nanProcessor = (_schema, ctx, _json, _params) => {
105313
+ if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
105314
+ };
105315
+ var templateLiteralProcessor = (schema, _ctx, json, _params) => {
105316
+ const _json = json;
105317
+ const pattern = schema._zod.pattern;
105318
+ if (!pattern) throw new Error("Pattern not found in template literal");
105319
+ _json.type = "string";
105320
+ _json.pattern = pattern.source;
105321
+ };
105322
+ var fileProcessor = (schema, _ctx, json, _params) => {
105323
+ const _json = json;
105324
+ const file = {
105325
+ type: "string",
105326
+ format: "binary",
105327
+ contentEncoding: "binary"
105328
+ };
105329
+ const { minimum, maximum, mime } = schema._zod.bag;
105330
+ if (minimum !== void 0) file.minLength = minimum;
105331
+ if (maximum !== void 0) file.maxLength = maximum;
105332
+ if (mime) if (mime.length === 1) {
105333
+ file.contentMediaType = mime[0];
105334
+ Object.assign(_json, file);
105335
+ } else {
105336
+ Object.assign(_json, file);
105337
+ _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
105338
+ }
105339
+ else Object.assign(_json, file);
105340
+ };
105341
+ var successProcessor = (_schema, _ctx, json, _params) => {
105342
+ json.type = "boolean";
105343
+ };
105301
105344
  var customProcessor = (_schema, ctx, _json, _params) => {
105302
105345
  if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
105303
105346
  };
105347
+ var functionProcessor = (_schema, ctx, _json, _params) => {
105348
+ if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
105349
+ };
105304
105350
  var transformProcessor = (_schema, ctx, _json, _params) => {
105305
105351
  if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
105306
105352
  };
105353
+ var mapProcessor = (_schema, ctx, _json, _params) => {
105354
+ if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
105355
+ };
105356
+ var setProcessor = (_schema, ctx, _json, _params) => {
105357
+ if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
105358
+ };
105307
105359
  var arrayProcessor = (schema, ctx, _json, params) => {
105308
105360
  const json = _json;
105309
105361
  const def = schema._zod.def;
@@ -105508,6 +105560,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
105508
105560
  seen.ref = def.innerType;
105509
105561
  json.readOnly = true;
105510
105562
  };
105563
+ var promiseProcessor = (schema, ctx, _json, params) => {
105564
+ const def = schema._zod.def;
105565
+ process$1(def.innerType, ctx, params);
105566
+ const seen = ctx.seen.get(schema);
105567
+ seen.ref = def.innerType;
105568
+ };
105511
105569
  var optionalProcessor = (schema, ctx, _json, params) => {
105512
105570
  const def = schema._zod.def;
105513
105571
  process$1(def.innerType, ctx, params);
@@ -105520,6 +105578,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
105520
105578
  const seen = ctx.seen.get(schema);
105521
105579
  seen.ref = innerType;
105522
105580
  };
105581
+ var allProcessors = {
105582
+ string: stringProcessor,
105583
+ number: numberProcessor,
105584
+ boolean: booleanProcessor,
105585
+ bigint: bigintProcessor,
105586
+ symbol: symbolProcessor,
105587
+ null: nullProcessor,
105588
+ undefined: undefinedProcessor,
105589
+ void: voidProcessor,
105590
+ never: neverProcessor,
105591
+ any: anyProcessor,
105592
+ unknown: unknownProcessor,
105593
+ date: dateProcessor,
105594
+ enum: enumProcessor,
105595
+ literal: literalProcessor,
105596
+ nan: nanProcessor,
105597
+ template_literal: templateLiteralProcessor,
105598
+ file: fileProcessor,
105599
+ success: successProcessor,
105600
+ custom: customProcessor,
105601
+ function: functionProcessor,
105602
+ transform: transformProcessor,
105603
+ map: mapProcessor,
105604
+ set: setProcessor,
105605
+ array: arrayProcessor,
105606
+ object: objectProcessor,
105607
+ union: unionProcessor,
105608
+ intersection: intersectionProcessor,
105609
+ tuple: tupleProcessor,
105610
+ record: recordProcessor,
105611
+ nullable: nullableProcessor,
105612
+ nonoptional: nonoptionalProcessor,
105613
+ default: defaultProcessor,
105614
+ prefault: prefaultProcessor,
105615
+ catch: catchProcessor,
105616
+ pipe: pipeProcessor,
105617
+ readonly: readonlyProcessor,
105618
+ promise: promiseProcessor,
105619
+ optional: optionalProcessor,
105620
+ lazy: lazyProcessor
105621
+ };
105622
+ function toJSONSchema(input, params) {
105623
+ if ("_idmap" in input) {
105624
+ const registry = input;
105625
+ const ctx = initializeContext({
105626
+ ...params,
105627
+ processors: allProcessors
105628
+ });
105629
+ const defs = {};
105630
+ for (const entry of registry._idmap.entries()) {
105631
+ const [_, schema] = entry;
105632
+ process$1(schema, ctx);
105633
+ }
105634
+ const schemas = {};
105635
+ ctx.external = {
105636
+ registry,
105637
+ uri: params?.uri,
105638
+ defs
105639
+ };
105640
+ for (const entry of registry._idmap.entries()) {
105641
+ const [key, schema] = entry;
105642
+ extractDefs(ctx, schema);
105643
+ schemas[key] = finalize(ctx, schema);
105644
+ }
105645
+ if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
105646
+ return { schemas };
105647
+ }
105648
+ const ctx = initializeContext({
105649
+ ...params,
105650
+ processors: allProcessors
105651
+ });
105652
+ process$1(input, ctx);
105653
+ extractDefs(ctx, input);
105654
+ return finalize(ctx, input);
105655
+ }
105523
105656
  //#endregion
105524
105657
  //#region ../../node_modules/zod/v4/classic/iso.js
105525
105658
  var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
@@ -106544,7 +106677,7 @@ var ZodIssueCode = {
106544
106677
  var ZodFirstPartyTypeKind;
106545
106678
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
106546
106679
  //#endregion
106547
- //#region ../types/dist/sleep-Cv4Zmie9.mjs
106680
+ //#region ../types/dist/sleep-D1jjWAr8.mjs
106548
106681
  /**
106549
106682
  * The audio chunk plane's byte format, and the ONE expansion from a coded
106550
106683
  * window to float samples (D455).
@@ -106699,6 +106832,9 @@ function isSecretMarkerLike(value) {
106699
106832
  function isSecretClear(value) {
106700
106833
  return isRecord$7(value) && value["$secretClear"] === true;
106701
106834
  }
106835
+ function isSecretFieldView(value) {
106836
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
106837
+ }
106702
106838
  /**
106703
106839
  * The UI view of a stored secret field value: a marker becomes "set", inline
106704
106840
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -107350,11 +107486,36 @@ function projectStore(store, perNodeKeys, nodeId) {
107350
107486
  }
107351
107487
  return out;
107352
107488
  }
107489
+ /**
107490
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
107491
+ *
107492
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
107493
+ * scope (global, one node, one device, one integration, one row of a business
107494
+ * collection) and the field path inside that scope — so saving the same field
107495
+ * twice updates the same ref, and the ref itself says which settings blob
107496
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
107497
+ * copied onto another ref does not decrypt.
107498
+ *
107499
+ * Array items are addressed by ITEM ID, never by index
107500
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
107501
+ *
107502
+ * Pure and browser-safe: no node builtins, no I/O.
107503
+ */
107504
+ /** The URI scheme of every keychain ref. */
107505
+ var SECRET_REF_SCHEME = "secret://";
107353
107506
  var GLOBAL_SCOPE = { kind: "global" };
107354
107507
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
107355
107508
  function globalSecretScope() {
107356
107509
  return GLOBAL_SCOPE;
107357
107510
  }
107511
+ /**
107512
+ * An addon id as a ref owner. A hub in-process builtin's context id is
107513
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
107514
+ * addon running forked.
107515
+ */
107516
+ function normalizeSecretOwner(addonId) {
107517
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
107518
+ }
107358
107519
  function encodeSegment(value) {
107359
107520
  return encodeURIComponent(value);
107360
107521
  }
@@ -107365,6 +107526,16 @@ function decodeSegment(value) {
107365
107526
  return null;
107366
107527
  }
107367
107528
  }
107529
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
107530
+ function formatSecretScope(scope) {
107531
+ switch (scope.kind) {
107532
+ case "global": return "global";
107533
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
107534
+ case "device": return `device/${scope.deviceId}`;
107535
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
107536
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
107537
+ }
107538
+ }
107368
107539
  /**
107369
107540
  * The field path of a value inside an array item, addressed by the item's id:
107370
107541
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -107373,6 +107544,13 @@ function decodeSegment(value) {
107373
107544
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
107374
107545
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
107375
107546
  }
107547
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
107548
+ function buildSecretRef(parts) {
107549
+ const owner = normalizeSecretOwner(parts.owner);
107550
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
107551
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
107552
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
107553
+ }
107376
107554
  function parseScope(segments) {
107377
107555
  switch (segments[0]) {
107378
107556
  case "global": return {
@@ -109285,6 +109463,45 @@ function normalizeAddonInitResult(result) {
109285
109463
  if (Array.isArray(result)) return { providers: result };
109286
109464
  return result;
109287
109465
  }
109466
+ var PREFIX = "records/";
109467
+ /** The base of every records failure. `detail` is the human part of the message. */
109468
+ var RecordsError = class extends Error {
109469
+ code;
109470
+ detail;
109471
+ constructor(code, detail) {
109472
+ super(`${PREFIX}${code}: ${detail}`);
109473
+ this.name = "RecordsError";
109474
+ this.code = code;
109475
+ this.detail = detail;
109476
+ }
109477
+ };
109478
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
109479
+ var RevConflictError = class extends RecordsError {
109480
+ constructor(detail) {
109481
+ super("rev-conflict", detail);
109482
+ this.name = "RevConflictError";
109483
+ }
109484
+ };
109485
+ /** The keychain could not store or serve a value: explicit, never an empty string. */
109486
+ var RecordSecretUnavailableError = class extends RecordsError {
109487
+ constructor(detail) {
109488
+ super("secret-unavailable", detail);
109489
+ this.name = "RecordSecretUnavailableError";
109490
+ }
109491
+ };
109492
+ var InvalidRecordError = class extends RecordsError {
109493
+ constructor(detail) {
109494
+ super("invalid-record", detail);
109495
+ this.name = "InvalidRecordError";
109496
+ }
109497
+ };
109498
+ /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
109499
+ var DeclarationRefusedError = class extends RecordsError {
109500
+ constructor(detail) {
109501
+ super("declaration-refused", detail);
109502
+ this.name = "DeclarationRefusedError";
109503
+ }
109504
+ };
109288
109505
  /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
109289
109506
  /** The files a kind may live in. `history` is device-history's (D850), never records. */
109290
109507
  var RECORD_DATABASES = ["settings", "events"];
@@ -109300,6 +109517,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
109300
109517
  var RECORD_MAX_DATA_BYTES = 1024 * 1024;
109301
109518
  var RECORD_BATCH_MAX = 1e3;
109302
109519
  var RECORD_QUERY_MAX = 1e3;
109520
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
109521
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
109522
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
109523
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
109524
+ /**
109525
+ * Secret fields of a record, addressed the way a Zod schema declares them
109526
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
109527
+ * (`auth.accessToken`, `headers[].value`).
109528
+ *
109529
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
109530
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
109531
+ * key releases exactly that key's secret.
109532
+ *
109533
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
109534
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
109535
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
109536
+ * leaf names its element: `headers[X-Api].value`.
109537
+ *
109538
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
109539
+ */
109540
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
109541
+ function parseSecretPath(path) {
109542
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
109543
+ return path.split(".").map((raw) => {
109544
+ const each = raw.endsWith("[]");
109545
+ const map = raw.endsWith("{}");
109546
+ const key = each || map ? raw.slice(0, -2) : raw;
109547
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
109548
+ return {
109549
+ key,
109550
+ each,
109551
+ map
109552
+ };
109553
+ });
109554
+ }
109555
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
109556
+ function secretArrayPrefixes(path) {
109557
+ const segments = parseSecretPath(path);
109558
+ const prefixes = [];
109559
+ const walked = [];
109560
+ for (const segment of segments) {
109561
+ walked.push(segment.key);
109562
+ if (segment.each) prefixes.push(walked.join("."));
109563
+ }
109564
+ return prefixes;
109565
+ }
109566
+ function isRecord$4(value) {
109567
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109568
+ }
109569
+ function elementId(element, idKey, declared, prefix) {
109570
+ const id = isRecord$4(element) ? element[idKey] : void 0;
109571
+ if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
109572
+ return id;
109573
+ }
109574
+ /**
109575
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
109576
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
109577
+ * result is a new body, `data` is untouched.
109578
+ */
109579
+ var REMOVE_LEAF = Symbol("remove-leaf");
109580
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
109581
+ if (!isRecord$4(node)) return node;
109582
+ const segment = segments[index];
109583
+ if (segment === void 0) return node;
109584
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
109585
+ const child = node[segment.key];
109586
+ const keys = [...walkedKeys, segment.key];
109587
+ const last = index === segments.length - 1;
109588
+ if (segment.each) {
109589
+ if (!Array.isArray(child)) return node;
109590
+ const prefix = keys.join(".");
109591
+ const idKey = arrayKeys[prefix];
109592
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
109593
+ const mapped = child.map((element) => {
109594
+ const id = elementId(element, idKey, declared, prefix);
109595
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
109596
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
109597
+ });
109598
+ return {
109599
+ ...node,
109600
+ [segment.key]: mapped
109601
+ };
109602
+ }
109603
+ if (segment.map) {
109604
+ if (!isRecord$4(child)) return node;
109605
+ const kept = Object.entries(child).map(([mapKey, value]) => {
109606
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
109607
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
109608
+ return [mapKey, visit({
109609
+ declared,
109610
+ concrete,
109611
+ value
109612
+ })];
109613
+ }).filter(([, value]) => value !== REMOVE_LEAF);
109614
+ return {
109615
+ ...node,
109616
+ [segment.key]: Object.fromEntries(kept)
109617
+ };
109618
+ }
109619
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
109620
+ if (last) {
109621
+ const replacement = visit({
109622
+ declared,
109623
+ concrete,
109624
+ value: child
109625
+ });
109626
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
109627
+ return {
109628
+ ...node,
109629
+ [segment.key]: replacement
109630
+ };
109631
+ }
109632
+ return {
109633
+ ...node,
109634
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
109635
+ };
109636
+ }
109637
+ /** Replace/remove every present secret leaf; returns a NEW body. */
109638
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
109639
+ let current = data;
109640
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
109641
+ return current;
109642
+ }
109643
+ /** Every present secret leaf, read-only. */
109644
+ function collectSecretLeaves(data, paths, arrayKeys) {
109645
+ const found = [];
109646
+ mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
109647
+ found.push(leaf);
109648
+ return leaf.value;
109649
+ });
109650
+ return found;
109651
+ }
109652
+ /**
109653
+ * The keychain collection name a kind's secrets live under. Prefixed so a
109654
+ * record's refs never collide with the refs a legacy collection of the same name
109655
+ * already holds during an adoption.
109656
+ */
109657
+ function recordSecretCollection(kind) {
109658
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
109659
+ }
109660
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
109661
+ function recordSecretRef(owner, kind, rowId, concretePath) {
109662
+ return buildSecretRef({
109663
+ owner,
109664
+ scope: {
109665
+ kind: "row",
109666
+ collection: recordSecretCollection(kind),
109667
+ rowId
109668
+ },
109669
+ fieldPath: concretePath
109670
+ });
109671
+ }
109672
+ /**
109673
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
109674
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
109675
+ * token. The form hands it to the secret field, which reveals the value by ref
109676
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
109677
+ * stored secret (the records door reads a view as KEEP).
109678
+ *
109679
+ * ONE implementation for the entries service and for every addon that serves
109680
+ * its own instances (`RecordIntegrationInstances`).
109681
+ */
109682
+ function isRecord$3(value) {
109683
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109684
+ }
109685
+ function secretViewsOf(owner, kind, id, decl, data) {
109686
+ if (decl.secretPaths.length === 0) return data;
109687
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
109688
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
109689
+ return {
109690
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
109691
+ v: 1,
109692
+ set: true
109693
+ };
109694
+ });
109695
+ return isRecord$3(shown) ? shown : data;
109696
+ }
109303
109697
  /**
109304
109698
  * The share-view scope kinds a share-link token can carry. The hub mints
109305
109699
  * and validates them (`ShareViewGrantSchema` builds its
@@ -109459,6 +109853,126 @@ var RecordKindDeclarationSchema = object({
109459
109853
  }).strict();
109460
109854
  /** The kind names: lower-case, dash separated, short enough for an index name. */
109461
109855
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
109856
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
109857
+ function assertValidDeclaration(name, decl) {
109858
+ const refuse = (why) => {
109859
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
109860
+ };
109861
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
109862
+ const seen = /* @__PURE__ */ new Set();
109863
+ for (const index of decl.indexes) {
109864
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
109865
+ seen.add(index.name);
109866
+ }
109867
+ const { retention } = decl;
109868
+ const hasSecrets = decl.secretPaths.length > 0;
109869
+ if ("ttlMs" in retention) {
109870
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
109871
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
109872
+ }
109873
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
109874
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
109875
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
109876
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
109877
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
109878
+ if (decl.database === "settings" && bound > 5e4) refuse(`a row bound above ${RECORD_SETTINGS_MAX_ROWS} belongs in database: "events" (a big delete on the settings thread stalls every configuration read)`);
109879
+ }
109880
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
109881
+ for (const path of decl.secretPaths) for (const prefix of secretArrayPrefixes(path)) if (decl.secretArrayKeys[prefix] === void 0) refuse(`secret path "${path}" crosses the array "${prefix}": declare its id key in secretArrayKeys`);
109882
+ }
109883
+ function bodyFieldsOf(schema) {
109884
+ return { ...toJSONSchema(schema, {
109885
+ unrepresentable: "any",
109886
+ io: "input"
109887
+ }) };
109888
+ }
109889
+ function isRecordLike(value) {
109890
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109891
+ }
109892
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
109893
+ function hasNameField(json, field) {
109894
+ const branches = json["oneOf"] ?? json["anyOf"];
109895
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
109896
+ const properties = json["properties"];
109897
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
109898
+ }
109899
+ /**
109900
+ * Declare the kinds of a package. The keys are the kind names. The result is
109901
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
109902
+ * types its handle from.
109903
+ */
109904
+ function defineRecordKinds(specs) {
109905
+ const out = {};
109906
+ for (const [name, spec] of Object.entries(specs)) {
109907
+ const secretPaths = collectZodSecretPaths(spec.schema);
109908
+ const nameField = spec.entry?.nameField ?? "name";
109909
+ const fields = bodyFieldsOf(spec.schema);
109910
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
109911
+ const parsed = RecordKindDeclarationSchema.parse({
109912
+ database: spec.database,
109913
+ class: spec.class,
109914
+ writeTrigger: spec.writeTrigger,
109915
+ device: spec.device,
109916
+ at: spec.at,
109917
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
109918
+ retention: spec.retention,
109919
+ deviceRemoval: spec.deviceRemoval,
109920
+ maxDataBytes: spec.maxDataBytes,
109921
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
109922
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
109923
+ transactFamily: spec.transactFamily,
109924
+ entry: spec.entry === void 0 ? void 0 : {
109925
+ title: spec.entry.title,
109926
+ nameField,
109927
+ fields
109928
+ }
109929
+ });
109930
+ assertValidDeclaration(name, parsed);
109931
+ out[name] = {
109932
+ name,
109933
+ schema: spec.schema,
109934
+ declaration: parsed
109935
+ };
109936
+ }
109937
+ return out;
109938
+ }
109939
+ /**
109940
+ * Entry kinds (D881): every list of user-created entries (notification
109941
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
109942
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
109943
+ * through the `record-entries` capability with password fields derived from
109944
+ * the schema (`.meta({ secret: true })`).
109945
+ */
109946
+ /** The body of a plain entry: a name and the fields the kind adds. */
109947
+ function entryBody(shape) {
109948
+ return object({
109949
+ name: string().min(1),
109950
+ ...shape
109951
+ });
109952
+ }
109953
+ /**
109954
+ * Declare one entry kind. An entry is configuration: never trimmed by the
109955
+ * engine, never aged, belonging to no device.
109956
+ */
109957
+ function defineEntryKind(name, spec) {
109958
+ const defined = defineRecordKinds({ [name]: {
109959
+ schema: spec.schema,
109960
+ class: "config",
109961
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
109962
+ retention: { none: "an entry goes only when the operator deletes it" },
109963
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
109964
+ ...spec.database === void 0 ? {} : { database: spec.database },
109965
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
109966
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
109967
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
109968
+ entry: {
109969
+ title: spec.title,
109970
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
109971
+ }
109972
+ } })[name];
109973
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
109974
+ return defined;
109975
+ }
109462
109976
  object({
109463
109977
  field: string().min(1),
109464
109978
  on: literal("advance")
@@ -113631,6 +114145,205 @@ var IntegrationInstanceRefSchema = object({
113631
114145
  ]),
113632
114146
  candidateStableId: string().optional()
113633
114147
  });
114148
+ /**
114149
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
114150
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
114151
+ *
114152
+ * Every instance is one record of the addon's own kind, keyed by the
114153
+ * `integrations` registry id. The records door does the secret work: a field the
114154
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
114155
+ * deleting the row deletes the secret, and one row's value is never another's.
114156
+ *
114157
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
114158
+ * token or the view the form was given KEEP the stored secret;
114159
+ * `{ $secretClear: true }` clears it; any other string replaces it.
114160
+ */
114161
+ /** The most instances one addon lists (an integrations page, not a table). */
114162
+ var INSTANCES_LIST_MAX = 1e3;
114163
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
114164
+ function topLevelSecretKeys(paths) {
114165
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
114166
+ }
114167
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
114168
+ function secretMapKeys(paths) {
114169
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
114170
+ }
114171
+ /**
114172
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
114173
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
114174
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
114175
+ */
114176
+ function secretMapPatch(value) {
114177
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
114178
+ const out = {};
114179
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
114180
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
114181
+ return Object.keys(out).length > 0 ? out : null;
114182
+ }
114183
+ /** Does the kind's body schema carry an `enabled` field? */
114184
+ function declaresEnabled(kind) {
114185
+ const json = toJSONSchema(kind.schema, {
114186
+ unrepresentable: "any",
114187
+ io: "input"
114188
+ });
114189
+ const properties = Reflect.get(json, "properties");
114190
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
114191
+ }
114192
+ var RecordIntegrationInstances = class {
114193
+ records;
114194
+ kind;
114195
+ hooks;
114196
+ handle;
114197
+ secretKeys;
114198
+ secretMaps;
114199
+ nameField;
114200
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
114201
+ registryOwnsEnabled;
114202
+ constructor(records, kind, hooks) {
114203
+ this.records = records;
114204
+ this.kind = kind;
114205
+ this.hooks = hooks;
114206
+ this.handle = records.kind(kind);
114207
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
114208
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
114209
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
114210
+ this.registryOwnsEnabled = hooks.description.role === "devices";
114211
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
114212
+ }
114213
+ /**
114214
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
114215
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
114216
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
114217
+ */
114218
+ async readResolved(id) {
114219
+ try {
114220
+ const record = await this.handle.getResolved(id);
114221
+ return record === null ? null : {
114222
+ id,
114223
+ body: record.data,
114224
+ enabled: record.data.enabled !== false,
114225
+ secrets: "resolved"
114226
+ };
114227
+ } catch (err) {
114228
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
114229
+ const stored = await this.handle.get(id);
114230
+ if (stored === null) return null;
114231
+ return {
114232
+ id,
114233
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
114234
+ enabled: stored.data.enabled !== false,
114235
+ secrets: "unavailable"
114236
+ };
114237
+ }
114238
+ }
114239
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
114240
+ async getResolved(id) {
114241
+ return (await this.readResolved(id))?.body ?? null;
114242
+ }
114243
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
114244
+ async listResolved() {
114245
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
114246
+ const out = [];
114247
+ for (const record of page.records) {
114248
+ const read = await this.readResolved(record.id);
114249
+ if (read !== null) out.push(read);
114250
+ }
114251
+ return out;
114252
+ }
114253
+ viewOf(record) {
114254
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
114255
+ const rawName = record.data[this.nameField];
114256
+ const kind = this.hooks.kindOf?.(record.data);
114257
+ const health = this.hooks.health?.(record.data, record.id);
114258
+ return {
114259
+ id: record.id,
114260
+ name: typeof rawName === "string" ? rawName : record.id,
114261
+ configured: this.hooks.configured(record.data),
114262
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
114263
+ formValues: shown,
114264
+ ...kind !== void 0 ? { kind } : {},
114265
+ ...health?.status !== void 0 ? { status: health.status } : {},
114266
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
114267
+ };
114268
+ }
114269
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
114270
+ ...this.hooks.description,
114271
+ mintsId: true
114272
+ };
114273
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
114274
+ listInstances = async () => {
114275
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
114276
+ };
114277
+ getInstance = async ({ id }) => {
114278
+ const record = await this.handle.get(id);
114279
+ return record === null ? null : this.viewOf(record);
114280
+ };
114281
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
114282
+ createInstance = async ({ id, name, kind }) => {
114283
+ const minted = id ?? this.hooks.mintId?.({
114284
+ name,
114285
+ ...kind !== void 0 ? { kind } : {}
114286
+ });
114287
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
114288
+ await this.handle.put({
114289
+ id: minted,
114290
+ data: this.hooks.initialBody(name, kind)
114291
+ }, { ifRev: 0 });
114292
+ await this.hooks.onChanged?.(minted, "created");
114293
+ return { id: minted };
114294
+ };
114295
+ updateInstance = async ({ id, name, values }) => {
114296
+ const patch = {};
114297
+ for (const [key, value] of Object.entries(values)) {
114298
+ if (value === void 0) continue;
114299
+ if (this.secretMaps.has(key)) {
114300
+ const entries = secretMapPatch(value);
114301
+ if (entries !== null) patch[key] = entries;
114302
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
114303
+ else if (isSecretClear(value)) patch[key] = null;
114304
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
114305
+ }
114306
+ if (name !== void 0) patch[this.nameField] = name;
114307
+ await this.handle.patch(id, patch);
114308
+ await this.hooks.onChanged?.(id, "updated");
114309
+ return { ok: true };
114310
+ };
114311
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
114312
+ setInstanceEnabled = async ({ id, enabled }) => {
114313
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
114314
+ await this.handle.patch(id, { enabled });
114315
+ await this.hooks.onChanged?.(id, "enabled");
114316
+ return { ok: true };
114317
+ };
114318
+ /** The row and its keychain refs go together (the records door). */
114319
+ deleteInstance = async ({ id }) => {
114320
+ await this.handle.delete(id);
114321
+ await this.hooks.onChanged?.(id, "deleted");
114322
+ return { ok: true };
114323
+ };
114324
+ testInstance = async (input) => {
114325
+ if (this.hooks.test === void 0) return {
114326
+ success: false,
114327
+ status: "unsupported",
114328
+ error: "This integration declares no connection test"
114329
+ };
114330
+ return this.hooks.test(input);
114331
+ };
114332
+ /** The flat provider object to hand to `registerProvider`. */
114333
+ toProvider() {
114334
+ return {
114335
+ describe: this.describe,
114336
+ instanceFormSchema: this.instanceFormSchema,
114337
+ listInstances: this.listInstances,
114338
+ getInstance: this.getInstance,
114339
+ createInstance: this.createInstance,
114340
+ updateInstance: this.updateInstance,
114341
+ setInstanceEnabled: this.setInstanceEnabled,
114342
+ deleteInstance: this.deleteInstance,
114343
+ testInstance: this.testInstance
114344
+ };
114345
+ }
114346
+ };
113634
114347
  var EncodeProfileSchema = object({
113635
114348
  video: object({
113636
114349
  codec: _enum([
@@ -114735,6 +115448,130 @@ var MigrationOwnerStepSchema = object({
114735
115448
  /** The ledger state the engine found: `pending` has no row yet. */
114736
115449
  state: union([literal("pending"), MigrationStatusSchema])
114737
115450
  });
115451
+ /**
115452
+ * Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
115453
+ * kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
115454
+ * the integration id, the devices stamped with it and the automations that name it do not change.
115455
+ *
115456
+ * Order, so a crash or a locked keychain is safe at every point:
115457
+ *
115458
+ * 1. Phase A, read only: every row of every integration of the addon is read with its secrets
115459
+ * resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
115460
+ * anything is written; `block-owner` then holds the addon.
115461
+ * 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
115462
+ * `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
115463
+ * and every secret leaf must equal what the rows held, else the record is deleted and the pass
115464
+ * throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
115465
+ * a warning names the path. Only then are the rows (and their keychain values) cleared.
115466
+ *
115467
+ * Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
115468
+ * a no-op. A default integration with no rows still gets its record (an empty body, D985).
115469
+ * Never logs or throws a secret value; messages name integration ids and secret PATHS only.
115470
+ */
115471
+ function secretLeavesOf(body, spec) {
115472
+ const out = /* @__PURE__ */ new Map();
115473
+ for (const leaf of collectSecretLeaves(body, spec.secretPaths, spec.secretArrayKeys ?? {})) if (typeof leaf.value === "string" && leaf.value.length > 0) out.set(leaf.concrete, leaf.value);
115474
+ return out;
115475
+ }
115476
+ function why(err) {
115477
+ return err instanceof Error ? err.message : String(err);
115478
+ }
115479
+ async function moveIntegrationRowsToRecords(ports, spec) {
115480
+ const integrations = await ports.listIntegrations(spec.addonId);
115481
+ if (integrations.length === 0) return {
115482
+ outcome: "already-done",
115483
+ summary: { integrations: 0 }
115484
+ };
115485
+ const rowsOf = /* @__PURE__ */ new Map();
115486
+ for (const integration of integrations) try {
115487
+ rowsOf.set(integration.id, await ports.readRows(integration.id));
115488
+ } catch (err) {
115489
+ throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
115490
+ }
115491
+ let created = 0;
115492
+ let cleared = 0;
115493
+ let kept = 0;
115494
+ for (const integration of integrations) {
115495
+ const values = rowsOf.get(integration.id) ?? {};
115496
+ const hasRows = Object.keys(values).length > 0;
115497
+ const body = spec.bodyFrom(values, ports, integration);
115498
+ const put = await ports.putRecord(integration.id, body);
115499
+ if (put === "created") created += 1;
115500
+ if (!hasRows) continue;
115501
+ const wanted = secretLeavesOf(body, spec);
115502
+ const back = await ports.readRecord(integration.id);
115503
+ if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
115504
+ const held = secretLeavesOf(back, spec);
115505
+ if (put === "created") {
115506
+ const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
115507
+ if (wrong.length > 0) {
115508
+ await ports.deleteRecord(integration.id);
115509
+ throw new Error(`integration '${integration.id}': ${wrong.join(", ")} did not read back from the record - the record was removed and the legacy settings are kept`);
115510
+ }
115511
+ } else {
115512
+ const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
115513
+ if (missing.length > 0) {
115514
+ ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
115515
+ kept += 1;
115516
+ continue;
115517
+ }
115518
+ }
115519
+ await ports.clearRows(integration.id);
115520
+ cleared += 1;
115521
+ }
115522
+ return created === 0 && cleared === 0 ? {
115523
+ outcome: "already-done",
115524
+ summary: {
115525
+ integrations: integrations.length,
115526
+ kept
115527
+ }
115528
+ } : {
115529
+ outcome: "done",
115530
+ summary: {
115531
+ integrations: integrations.length,
115532
+ created,
115533
+ cleared,
115534
+ kept
115535
+ }
115536
+ };
115537
+ }
115538
+ /**
115539
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
115540
+ * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
115541
+ * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
115542
+ * the new record through the addon's own records handle. The hub deletes the rows and their
115543
+ * keychain values; the addon never touches the registry.
115544
+ */
115545
+ function integrationRowsPortsOverApi(deps) {
115546
+ return {
115547
+ warn: (message) => deps.log.warn(message),
115548
+ listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
115549
+ id: i.id,
115550
+ name: i.name
115551
+ })),
115552
+ readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
115553
+ putRecord: async (id, body) => {
115554
+ if (await deps.records.get(id) !== null) return "exists";
115555
+ try {
115556
+ await deps.records.put({
115557
+ id,
115558
+ data: body
115559
+ }, { ifRev: 0 });
115560
+ } catch (err) {
115561
+ if (err instanceof RevConflictError) return "exists";
115562
+ throw err;
115563
+ }
115564
+ return "created";
115565
+ },
115566
+ readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
115567
+ deleteRecord: async (id) => {
115568
+ await deps.records.delete(id);
115569
+ },
115570
+ clearRows: async (integrationId) => {
115571
+ await deps.api.clearSettings({ id: integrationId });
115572
+ }
115573
+ };
115574
+ }
114738
115575
  function ordinalOf(id) {
114739
115576
  const digits = id.replace(/\D+/g, "");
114740
115577
  return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
@@ -114809,6 +115646,40 @@ async function claimDevicesToDefaultIntegration(ports, spec) {
114809
115646
  }
114810
115647
  };
114811
115648
  }
115649
+ /**
115650
+ * The two migrations of a device addon whose integrations hold NO settings of their own
115651
+ * (planning#416, S-A): every integration becomes a record of the addon's own kind with a name and
115652
+ * nothing else (D985, the default integration is an instance with an empty body), then the devices
115653
+ * that carry no `integrationId` are claimed to the addon's default integration.
115654
+ *
115655
+ * 1. `rows-to-record` (block-owner): `moveIntegrationRowsToRecords` over the hub API. Rows an
115656
+ * addon of this shape does not own (a key nothing reads) are named in a warning, by key, and
115657
+ * dropped with the rows once the record is read back.
115658
+ * 2. `claim-devices` (tolerate): `claimDevicesToDefaultIntegration`; with no integration to claim
115659
+ * to it fails and is retried on the next boot.
115660
+ *
115661
+ * Both run in the addon process through the hub API (`ctx.api`), BEFORE the addon registers its
115662
+ * `integration-instances` provider: while a provider is registered the hub answers `getSettings`
115663
+ * from the record, not from the rows.
115664
+ */
115665
+ function defaultIntegrationPortsOver(deps) {
115666
+ return {
115667
+ ...integrationRowsPortsOverApi({
115668
+ api: deps.api,
115669
+ records: deps.records.kind({
115670
+ name: deps.kind.name,
115671
+ declaration: deps.kind.declaration,
115672
+ schema: record(string(), unknown())
115673
+ }),
115674
+ log: { warn: deps.warn }
115675
+ }),
115676
+ ...claimDevicesPortsOver({
115677
+ api: deps.api,
115678
+ info: deps.info,
115679
+ warnDevice: deps.warnDevice
115680
+ })
115681
+ };
115682
+ }
114812
115683
  /** The hub side of the device claim, over `ctx.api`. */
114813
115684
  function claimDevicesPortsOver(deps) {
114814
115685
  return {
@@ -157074,32 +157945,57 @@ var IntegrationInstancePatchSchema = object({
157074
157945
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
157075
157946
  values: record(string(), unknown())
157076
157947
  });
157077
- method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
157078
- id: IntegrationInstanceIdSchema.optional(),
157079
- name: string().min(1),
157080
- kind: string().min(1).optional()
157081
- }), object({ id: string() }), {
157082
- kind: "mutation",
157083
- auth: "admin"
157084
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
157085
- kind: "mutation",
157086
- auth: "admin"
157087
- }), method(object({
157088
- id: IntegrationInstanceIdSchema,
157089
- enabled: boolean()
157090
- }), object({ ok: literal(true) }), {
157091
- kind: "mutation",
157092
- auth: "admin"
157093
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
157094
- kind: "mutation",
157095
- auth: "admin"
157096
- }), optionalMethod(object({
157097
- id: IntegrationInstanceIdSchema.nullable(),
157098
- values: record(string(), unknown())
157099
- }), TestConnectionResultSchema$1, {
157100
- kind: "mutation",
157101
- auth: "admin"
157102
- });
157948
+ var integrationInstancesCapability = {
157949
+ name: "integration-instances",
157950
+ scope: "system",
157951
+ mode: "collection",
157952
+ methods: {
157953
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
157954
+ /** The form; `id: null` is the create form. */
157955
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
157956
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
157957
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
157958
+ /**
157959
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
157960
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
157961
+ */
157962
+ createInstance: method(object({
157963
+ id: IntegrationInstanceIdSchema.optional(),
157964
+ name: string().min(1),
157965
+ kind: string().min(1).optional()
157966
+ }), object({ id: string() }), {
157967
+ kind: "mutation",
157968
+ auth: "admin"
157969
+ }),
157970
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
157971
+ kind: "mutation",
157972
+ auth: "admin"
157973
+ }),
157974
+ /**
157975
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
157976
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
157977
+ */
157978
+ setInstanceEnabled: method(object({
157979
+ id: IntegrationInstanceIdSchema,
157980
+ enabled: boolean()
157981
+ }), object({ ok: literal(true) }), {
157982
+ kind: "mutation",
157983
+ auth: "admin"
157984
+ }),
157985
+ /** Delete the record and its keychain refs together. */
157986
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
157987
+ kind: "mutation",
157988
+ auth: "admin"
157989
+ }),
157990
+ testInstance: optionalMethod(object({
157991
+ id: IntegrationInstanceIdSchema.nullable(),
157992
+ values: record(string(), unknown())
157993
+ }), TestConnectionResultSchema$1, {
157994
+ kind: "mutation",
157995
+ auth: "admin"
157996
+ })
157997
+ }
157998
+ };
157103
157999
  /**
157104
158000
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
157105
158001
  * session (server-initiated offer pattern, identical to the main
@@ -220350,7 +221246,7 @@ function text(stored, key) {
220350
221246
  const value = stored[key];
220351
221247
  return typeof value === "string" && value.length > 0 ? value : null;
220352
221248
  }
220353
- function portOf(stored, ports) {
221249
+ function portOf$1(stored, ports) {
220354
221250
  const value = stored["emailPushPort"];
220355
221251
  const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
220356
221252
  if (Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535) return numeric;
@@ -220372,7 +221268,7 @@ async function moveEmailPushToMailbox(ports) {
220372
221268
  };
220373
221269
  const bindHost = text(stored, "emailPushBindHost");
220374
221270
  await ports.stashPending({
220375
- port: portOf(stored, ports),
221271
+ port: portOf$1(stored, ports),
220376
221272
  ...bindHost === null ? {} : { bindHost },
220377
221273
  domain: text(stored, "emailPushDomain") ?? DEFAULT_DOMAIN,
220378
221274
  requireAuth: stored["emailPushRequireAuth"] !== false,
@@ -220384,6 +221280,188 @@ async function moveEmailPushToMailbox(ports) {
220384
221280
  summary: { moved: true }
220385
221281
  };
220386
221282
  }
221283
+ var reolinkConnectionsEntryKind = defineEntryKind("connections", {
221284
+ title: "Reolink integrations",
221285
+ schema: entryBody({
221286
+ host: string().nullish(),
221287
+ port: number().int().min(1).max(65535).nullish(),
221288
+ rtspPort: number().int().min(1).max(65535).nullish(),
221289
+ username: string().nullish(),
221290
+ password: string().optional().meta({ secret: true }),
221291
+ transport: _enum(["tcp", "udp"]).nullish(),
221292
+ uid: string().nullish(),
221293
+ debugGeneral: boolean().nullish(),
221294
+ debugSocketLogs: array(ReolinkSocketDebugFlagSchema).nullish()
221295
+ }),
221296
+ writeTrigger: "an operator adds, edits or removes a Reolink integration, or a Hub device is migrated into one"
221297
+ });
221298
+ /** Every kind this addon declares, keyed by its name. */
221299
+ var reolinkRecordKinds = { connections: reolinkConnectionsEntryKind };
221300
+ /** The record body as the connection form's flat values: no name, no unset keys. */
221301
+ function hubSettingsOf(body) {
221302
+ const out = {};
221303
+ for (const [key, value] of Object.entries(body)) {
221304
+ if (key === "name" || value === null || value === void 0) continue;
221305
+ out[key] = value;
221306
+ }
221307
+ return out;
221308
+ }
221309
+ /**
221310
+ * What a resolved record says about its hub. A keychain that is locked yields a record WITHOUT
221311
+ * its password: that is `password-locked`, never an empty password (D49).
221312
+ */
221313
+ function hubConnectionOf(resolved) {
221314
+ const read = readHubConnection(hubSettingsOf(resolved.body));
221315
+ return read.kind === "no-password" && resolved.secrets === "unavailable" ? { kind: "password-locked" } : read;
221316
+ }
221317
+ /**
221318
+ * What each enabled integration's RECORD says about its hub: the one reader of the connection.
221319
+ * A record that is absent reads as `no-host` (no session), exactly as an empty settings blob did.
221320
+ */
221321
+ async function readHubConnections(instances, integrations) {
221322
+ const out = [];
221323
+ for (const integration of integrations) {
221324
+ const resolved = await instances.readResolved(integration.id);
221325
+ out.push({
221326
+ id: integration.id,
221327
+ name: integration.name,
221328
+ read: resolved === null ? { kind: "no-host" } : hubConnectionOf(resolved)
221329
+ });
221330
+ }
221331
+ return out;
221332
+ }
221333
+ function buildReolinkInstances(records, deps) {
221334
+ return new RecordIntegrationInstances(records, reolinkConnectionsEntryKind, {
221335
+ description: {
221336
+ role: "devices",
221337
+ discovery: {
221338
+ mode: "lan",
221339
+ scopedToInstance: false
221340
+ },
221341
+ instanceMode: "multiple",
221342
+ kind: "connection",
221343
+ canTest: false
221344
+ },
221345
+ formSchema: () => buildHubInstanceSchema(),
221346
+ initialBody: (name) => ({ name }),
221347
+ configured: (body) => (body.host ?? "").length === 0 || (body.password ?? "").length > 0,
221348
+ onChanged: () => deps.onChanged()
221349
+ });
221350
+ }
221351
+ /**
221352
+ * The provider handed to the hub: the default integration (no connection) shows no hub form, an
221353
+ * NVR / Home Hub one does, and a create wizard (no id yet) sets up a hub.
221354
+ */
221355
+ function reolinkInstancesProvider(instances, deps) {
221356
+ return {
221357
+ ...instances.toProvider(),
221358
+ instanceFormSchema: async ({ id }) => {
221359
+ if (id === null) return buildHubInstanceSchema();
221360
+ const [evidence, body] = await Promise.all([deps.registryEvidenceOf(id), instances.getResolved(id)]);
221361
+ return evidence !== null && isHubIntegration({
221362
+ originStableId: evidence.originStableId,
221363
+ info: evidence.info,
221364
+ settings: body === null ? {} : hubSettingsOf(body)
221365
+ }) || body === null ? buildHubInstanceSchema() : { sections: [] };
221366
+ }
221367
+ };
221368
+ }
221369
+ //#endregion
221370
+ //#region src/migrations/integration-rows-to-record.ts
221371
+ /**
221372
+ * Every Reolink integration moves from legacy `integration_settings` rows into its own
221373
+ * `connections` record under the registry id (planning#416, D1007). An NVR / Home Hub integration
221374
+ * carries its connection (host, ports, username, transport, uid, the two debug fields) and its
221375
+ * password, a keychain secret declared by the kind; the default integration carries a name only.
221376
+ * Each integration's record is built from ITS OWN rows, never from another's.
221377
+ *
221378
+ * Keys nothing reads any more are named in a warning, by key, and dropped with the rows once the
221379
+ * record has been read back. A value of the wrong type is left out and named by key as well.
221380
+ */
221381
+ var REOLINK_ROWS_TO_RECORD_ID = "provider-reolink/2026-11-05-integration-rows-to-record";
221382
+ var TEXT_KEYS = [
221383
+ "host",
221384
+ "username",
221385
+ "uid"
221386
+ ];
221387
+ var PORT_KEYS = ["port", "rtspPort"];
221388
+ function portOf(value) {
221389
+ const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
221390
+ return typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
221391
+ }
221392
+ function textOf(value) {
221393
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
221394
+ }
221395
+ /** The record body of one integration, from its resolved legacy rows. Never logs a value. */
221396
+ function reolinkBodyFromRows(values, log, integration) {
221397
+ const body = { name: integration.name };
221398
+ const unusable = [];
221399
+ const known = new Set([
221400
+ ...TEXT_KEYS,
221401
+ ...PORT_KEYS,
221402
+ "password",
221403
+ "transport",
221404
+ "debugGeneral",
221405
+ "debugSocketLogs"
221406
+ ]);
221407
+ for (const key of TEXT_KEYS) {
221408
+ if (!(key in values)) continue;
221409
+ const text = textOf(values[key]);
221410
+ if (text === null) unusable.push(key);
221411
+ else body[key] = text;
221412
+ }
221413
+ for (const key of PORT_KEYS) {
221414
+ if (!(key in values)) continue;
221415
+ const port = portOf(values[key]);
221416
+ if (port === null) unusable.push(key);
221417
+ else body[key] = port;
221418
+ }
221419
+ if ("password" in values) {
221420
+ const password = values["password"];
221421
+ if (typeof password === "string" && password.length > 0) body["password"] = password;
221422
+ else unusable.push("password");
221423
+ }
221424
+ if ("transport" in values) {
221425
+ const transport = values["transport"];
221426
+ if (transport === "tcp" || transport === "udp") body["transport"] = transport;
221427
+ else unusable.push("transport");
221428
+ }
221429
+ if ("debugGeneral" in values) {
221430
+ const flag = values["debugGeneral"];
221431
+ if (typeof flag === "boolean") body["debugGeneral"] = flag;
221432
+ else unusable.push("debugGeneral");
221433
+ }
221434
+ if ("debugSocketLogs" in values) {
221435
+ const flags = values["debugSocketLogs"];
221436
+ const parsed = Array.isArray(flags) ? flags.map((f) => ReolinkSocketDebugFlagSchema.safeParse(f)) : null;
221437
+ if (parsed !== null && parsed.every((p) => p.success)) body["debugSocketLogs"] = parsed.flatMap((p) => p.success ? [p.data] : []);
221438
+ else unusable.push("debugSocketLogs");
221439
+ }
221440
+ const unknown = Object.keys(values).filter((key) => !known.has(key));
221441
+ if (unknown.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${unknown.join(", ")}`);
221442
+ if (unusable.length > 0) log.warn(`integration '${integration.id}': legacy settings with an unusable value are left out: ${unusable.join(", ")}`);
221443
+ return reolinkConnectionsEntryKind.schema.parse(body);
221444
+ }
221445
+ var REOLINK_ROWS_TO_RECORD_MIGRATION = {
221446
+ meta: {
221447
+ id: REOLINK_ROWS_TO_RECORD_ID,
221448
+ owner: "provider-reolink",
221449
+ level: "addon-state",
221450
+ seq: 4,
221451
+ dependsOn: [],
221452
+ store: "settings",
221453
+ scope: "cluster",
221454
+ onFailure: "block-owner",
221455
+ bodyKind: "procedure",
221456
+ description: "Move every Reolink integration (the default one and each NVR / Home Hub) into its own connection record keyed by its registry id, the password as a keychain secret, then clear its legacy integration settings rows",
221457
+ fixtureMark: "a legacy setting key the migration does not know"
221458
+ },
221459
+ procedure: (ports) => moveIntegrationRowsToRecords(ports, {
221460
+ addonId: "provider-reolink",
221461
+ secretPaths: reolinkConnectionsEntryKind.declaration.secretPaths,
221462
+ bodyFrom: reolinkBodyFromRows
221463
+ })
221464
+ };
220387
221465
  /** The `provider-reolink` catalogue (D987): offered to the engine when the addon initialises. */
220388
221466
  var REOLINK_MIGRATIONS = [{
220389
221467
  meta: {
@@ -220417,17 +221495,17 @@ var REOLINK_MIGRATIONS = [{
220417
221495
  plan: planDropWebhookIntakeSettings
220418
221496
  }];
220419
221497
  /**
220420
- * Every persisted camera that names no integration is claimed to the addon's default one. The
220421
- * integration a hub migration created is never the default, so it is not eligible. The integration
220422
- * settings themselves stay where they are until this addon's own wave moves them.
221498
+ * The integration wave (D1007) in declaration order, which must follow `seq`: the device claim
221499
+ * shipped first (seq 3) and the rows move follows it (seq 4). The engine refuses the whole
221500
+ * catalogue when a seq does not increase, and the addon then does not start.
220423
221501
  */
220424
- var REOLINK_CLAIM_MIGRATION = claimDevicesMigration({
221502
+ var REOLINK_INTEGRATION_MIGRATIONS = [claimDevicesMigration({
220425
221503
  addonId: "provider-reolink",
220426
221504
  date: "2026-11-04",
220427
221505
  seq: 3,
220428
221506
  dependsOn: [],
220429
221507
  eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
220430
- });
221508
+ }), REOLINK_ROWS_TO_RECORD_MIGRATION];
220431
221509
  //#endregion
220432
221510
  //#region src/reolink-adoption-provider.ts
220433
221511
  var DEVICES_FILTER = {
@@ -220697,7 +221775,7 @@ var ReolinkHubManager = class {
220697
221775
  const { logger } = this.#deps;
220698
221776
  const wanted = /* @__PURE__ */ new Set();
220699
221777
  for (const integration of integrations) {
220700
- const read = readHubConnection(integration.settings);
221778
+ const { read } = integration;
220701
221779
  if (read.kind === "no-host") continue;
220702
221780
  wanted.add(integration.id);
220703
221781
  if (read.kind !== "ok") {
@@ -221065,6 +222143,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221065
222143
  */
221066
222144
  autodetectCache = new AutodetectCache();
221067
222145
  hubManager = null;
222146
+ instances = null;
221068
222147
  /**
221069
222148
  * The camera mailbox bridge (`email-push-bridge.ts`, D999): battery cameras mail motion to a
221070
222149
  * mailbox of the `smtp-inbound` broker, the bridge turns each message into the camera's
@@ -221310,25 +222389,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221310
222389
  }
221311
222390
  /**
221312
222391
  * Make the hub sessions match the live integration list: one per enabled integration with a
221313
- * host. Idempotent; runs at boot, after a migration and on every lifecycle event. When any
221314
- * integration's settings cannot be read the whole pass is skipped — an unreadable list must
222392
+ * host. Idempotent; runs at boot, after a migration, after an instance change and on every
222393
+ * lifecycle event. When any integration's record cannot be read the whole pass is skipped — an unreadable list must
221315
222394
  * never close a healthy session.
221316
222395
  */
221317
222396
  async reconcileIntegrations() {
221318
222397
  const manager = this.hubManager;
221319
- if (manager === null) return;
222398
+ const instances = this.instances;
222399
+ if (manager === null || instances === null) return;
221320
222400
  try {
221321
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
221322
- const withSettings = [];
221323
- for (const integration of mine) {
221324
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
221325
- withSettings.push({
221326
- id: integration.id,
221327
- name: integration.name,
221328
- settings
221329
- });
221330
- }
221331
- await manager.reconcile(withSettings);
222401
+ const withConnection = await readHubConnections(instances, (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled));
222402
+ await manager.reconcile(withConnection);
221332
222403
  } catch (err) {
221333
222404
  this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$3(err) } });
221334
222405
  }
@@ -221501,28 +222572,6 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221501
222572
  globalSettingsSchema() {
221502
222573
  return { sections: [...buildClipRefreshSettingsSchema().sections] };
221503
222574
  }
221504
- /**
221505
- * Per-instance form (D985): an NVR / Home Hub integration shows its connection only; the
221506
- * default integration keeps the full schema (`null`). A create wizard has no id yet and is
221507
- * always setting up a NEW instance — the default one already exists — so it gets the hub form.
221508
- */
221509
- async integrationSettingsSchema(instance) {
221510
- if (instance.integrationId === null) return buildHubInstanceSchema();
221511
- try {
221512
- const [integration, settings] = await Promise.all([this.ctx.api.integrations.get.query({ id: instance.integrationId }), this.ctx.api.integrations.getSettings.query({ id: instance.integrationId })]);
221513
- return isHubIntegration({
221514
- originStableId: integration.originStableId,
221515
- info: integration.info,
221516
- settings
221517
- }) ? buildHubInstanceSchema() : null;
221518
- } catch (err) {
221519
- this.ctx.logger.warn("Reolink integration form: instance lookup failed — full schema shown", { meta: {
221520
- integrationId: instance.integrationId,
221521
- error: errMsg$3(err)
221522
- } });
221523
- return null;
221524
- }
221525
- }
221526
222575
  async onInitialize() {
221527
222576
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
221528
222577
  this.logChannels = createLogChannelsProvider(this.ctx.logger.child("log-channels"));
@@ -221567,11 +222616,38 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221567
222616
  this.wireClipCatalogRefreshers();
221568
222617
  await this.serveClipPlanes(this.clipService);
221569
222618
  await this.serveWebhookPlane();
221570
- await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations([REOLINK_CLAIM_MIGRATION], claimDevicesPortsOver({
222619
+ const records = this.ctx.records;
222620
+ if (records === void 0) throw new Error("provider-reolink: ctx.records is required (hub-only addon)");
222621
+ await records.declare(reolinkRecordKinds);
222622
+ const integrationPorts = defaultIntegrationPortsOver({
221571
222623
  api: defaultIntegrationApiOver(this.ctx.api),
222624
+ records,
222625
+ kind: reolinkConnectionsEntryKind,
221572
222626
  info: (message) => this.ctx.logger.info(message),
222627
+ warn: (message) => this.ctx.logger.warn(message),
221573
222628
  warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
221574
- }))]);
222629
+ });
222630
+ const registryEvidenceOf = async (id) => {
222631
+ try {
222632
+ const integration = await this.ctx.api.integrations.get.query({ id });
222633
+ return {
222634
+ originStableId: integration.originStableId,
222635
+ info: { ...integration.info }
222636
+ };
222637
+ } catch (err) {
222638
+ this.ctx.logger.warn("Reolink integration form: instance lookup failed — hub form shown", { meta: {
222639
+ integrationId: id,
222640
+ error: errMsg$3(err)
222641
+ } });
222642
+ return null;
222643
+ }
222644
+ };
222645
+ const instances = buildReolinkInstances(records, {
222646
+ onChanged: () => this.reconcileIntegrations(),
222647
+ registryEvidenceOf
222648
+ });
222649
+ this.instances = instances;
222650
+ await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations(REOLINK_INTEGRATION_MIGRATIONS, integrationPorts)]);
221575
222651
  this.wireEmailBridge();
221576
222652
  const wireSleepPushResolvers = (event, via) => {
221577
222653
  const data = event.data;
@@ -221610,7 +222686,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221610
222686
  capability: deviceRemovalHookCapability,
221611
222687
  provider: createEmailPushRemovalHook({ resync: () => this.ensureEmailBridge().sync() })
221612
222688
  });
221613
- return regs;
222689
+ regs.push({
222690
+ capability: integrationInstancesCapability,
222691
+ provider: reolinkInstancesProvider(instances, { registryEvidenceOf })
222692
+ });
222693
+ return { providers: regs };
221614
222694
  }
221615
222695
  /** What the catalogue's migration reads and writes (D987). */
221616
222696
  migrationPorts() {