@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.mjs CHANGED
@@ -105246,6 +105246,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
105246
105246
  var booleanProcessor = (_schema, _ctx, json, _params) => {
105247
105247
  json.type = "boolean";
105248
105248
  };
105249
+ var bigintProcessor = (_schema, ctx, _json, _params) => {
105250
+ if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
105251
+ };
105252
+ var symbolProcessor = (_schema, ctx, _json, _params) => {
105253
+ if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
105254
+ };
105249
105255
  var nullProcessor = (_schema, ctx, json, _params) => {
105250
105256
  if (ctx.target === "openapi-3.0") {
105251
105257
  json.type = "string";
@@ -105253,12 +105259,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
105253
105259
  json.enum = [null];
105254
105260
  } else json.type = "null";
105255
105261
  };
105262
+ var undefinedProcessor = (_schema, ctx, _json, _params) => {
105263
+ if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
105264
+ };
105256
105265
  var voidProcessor = (_schema, ctx, _json, _params) => {
105257
105266
  if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
105258
105267
  };
105259
105268
  var neverProcessor = (_schema, _ctx, json, _params) => {
105260
105269
  json.not = {};
105261
105270
  };
105271
+ var anyProcessor = (_schema, _ctx, _json, _params) => {};
105272
+ var unknownProcessor = (_schema, _ctx, _json, _params) => {};
105262
105273
  var dateProcessor = (_schema, ctx, _json, _params) => {
105263
105274
  if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
105264
105275
  };
@@ -105290,12 +105301,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
105290
105301
  json.enum = vals;
105291
105302
  }
105292
105303
  };
105304
+ var nanProcessor = (_schema, ctx, _json, _params) => {
105305
+ if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
105306
+ };
105307
+ var templateLiteralProcessor = (schema, _ctx, json, _params) => {
105308
+ const _json = json;
105309
+ const pattern = schema._zod.pattern;
105310
+ if (!pattern) throw new Error("Pattern not found in template literal");
105311
+ _json.type = "string";
105312
+ _json.pattern = pattern.source;
105313
+ };
105314
+ var fileProcessor = (schema, _ctx, json, _params) => {
105315
+ const _json = json;
105316
+ const file = {
105317
+ type: "string",
105318
+ format: "binary",
105319
+ contentEncoding: "binary"
105320
+ };
105321
+ const { minimum, maximum, mime } = schema._zod.bag;
105322
+ if (minimum !== void 0) file.minLength = minimum;
105323
+ if (maximum !== void 0) file.maxLength = maximum;
105324
+ if (mime) if (mime.length === 1) {
105325
+ file.contentMediaType = mime[0];
105326
+ Object.assign(_json, file);
105327
+ } else {
105328
+ Object.assign(_json, file);
105329
+ _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
105330
+ }
105331
+ else Object.assign(_json, file);
105332
+ };
105333
+ var successProcessor = (_schema, _ctx, json, _params) => {
105334
+ json.type = "boolean";
105335
+ };
105293
105336
  var customProcessor = (_schema, ctx, _json, _params) => {
105294
105337
  if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
105295
105338
  };
105339
+ var functionProcessor = (_schema, ctx, _json, _params) => {
105340
+ if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
105341
+ };
105296
105342
  var transformProcessor = (_schema, ctx, _json, _params) => {
105297
105343
  if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
105298
105344
  };
105345
+ var mapProcessor = (_schema, ctx, _json, _params) => {
105346
+ if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
105347
+ };
105348
+ var setProcessor = (_schema, ctx, _json, _params) => {
105349
+ if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
105350
+ };
105299
105351
  var arrayProcessor = (schema, ctx, _json, params) => {
105300
105352
  const json = _json;
105301
105353
  const def = schema._zod.def;
@@ -105500,6 +105552,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
105500
105552
  seen.ref = def.innerType;
105501
105553
  json.readOnly = true;
105502
105554
  };
105555
+ var promiseProcessor = (schema, ctx, _json, params) => {
105556
+ const def = schema._zod.def;
105557
+ process$1(def.innerType, ctx, params);
105558
+ const seen = ctx.seen.get(schema);
105559
+ seen.ref = def.innerType;
105560
+ };
105503
105561
  var optionalProcessor = (schema, ctx, _json, params) => {
105504
105562
  const def = schema._zod.def;
105505
105563
  process$1(def.innerType, ctx, params);
@@ -105512,6 +105570,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
105512
105570
  const seen = ctx.seen.get(schema);
105513
105571
  seen.ref = innerType;
105514
105572
  };
105573
+ var allProcessors = {
105574
+ string: stringProcessor,
105575
+ number: numberProcessor,
105576
+ boolean: booleanProcessor,
105577
+ bigint: bigintProcessor,
105578
+ symbol: symbolProcessor,
105579
+ null: nullProcessor,
105580
+ undefined: undefinedProcessor,
105581
+ void: voidProcessor,
105582
+ never: neverProcessor,
105583
+ any: anyProcessor,
105584
+ unknown: unknownProcessor,
105585
+ date: dateProcessor,
105586
+ enum: enumProcessor,
105587
+ literal: literalProcessor,
105588
+ nan: nanProcessor,
105589
+ template_literal: templateLiteralProcessor,
105590
+ file: fileProcessor,
105591
+ success: successProcessor,
105592
+ custom: customProcessor,
105593
+ function: functionProcessor,
105594
+ transform: transformProcessor,
105595
+ map: mapProcessor,
105596
+ set: setProcessor,
105597
+ array: arrayProcessor,
105598
+ object: objectProcessor,
105599
+ union: unionProcessor,
105600
+ intersection: intersectionProcessor,
105601
+ tuple: tupleProcessor,
105602
+ record: recordProcessor,
105603
+ nullable: nullableProcessor,
105604
+ nonoptional: nonoptionalProcessor,
105605
+ default: defaultProcessor,
105606
+ prefault: prefaultProcessor,
105607
+ catch: catchProcessor,
105608
+ pipe: pipeProcessor,
105609
+ readonly: readonlyProcessor,
105610
+ promise: promiseProcessor,
105611
+ optional: optionalProcessor,
105612
+ lazy: lazyProcessor
105613
+ };
105614
+ function toJSONSchema(input, params) {
105615
+ if ("_idmap" in input) {
105616
+ const registry = input;
105617
+ const ctx = initializeContext({
105618
+ ...params,
105619
+ processors: allProcessors
105620
+ });
105621
+ const defs = {};
105622
+ for (const entry of registry._idmap.entries()) {
105623
+ const [_, schema] = entry;
105624
+ process$1(schema, ctx);
105625
+ }
105626
+ const schemas = {};
105627
+ ctx.external = {
105628
+ registry,
105629
+ uri: params?.uri,
105630
+ defs
105631
+ };
105632
+ for (const entry of registry._idmap.entries()) {
105633
+ const [key, schema] = entry;
105634
+ extractDefs(ctx, schema);
105635
+ schemas[key] = finalize(ctx, schema);
105636
+ }
105637
+ if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
105638
+ return { schemas };
105639
+ }
105640
+ const ctx = initializeContext({
105641
+ ...params,
105642
+ processors: allProcessors
105643
+ });
105644
+ process$1(input, ctx);
105645
+ extractDefs(ctx, input);
105646
+ return finalize(ctx, input);
105647
+ }
105515
105648
  //#endregion
105516
105649
  //#region ../../node_modules/zod/v4/classic/iso.js
105517
105650
  var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
@@ -106536,7 +106669,7 @@ var ZodIssueCode = {
106536
106669
  var ZodFirstPartyTypeKind;
106537
106670
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
106538
106671
  //#endregion
106539
- //#region ../types/dist/sleep-Cv4Zmie9.mjs
106672
+ //#region ../types/dist/sleep-D1jjWAr8.mjs
106540
106673
  /**
106541
106674
  * The audio chunk plane's byte format, and the ONE expansion from a coded
106542
106675
  * window to float samples (D455).
@@ -106691,6 +106824,9 @@ function isSecretMarkerLike(value) {
106691
106824
  function isSecretClear(value) {
106692
106825
  return isRecord$7(value) && value["$secretClear"] === true;
106693
106826
  }
106827
+ function isSecretFieldView(value) {
106828
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
106829
+ }
106694
106830
  /**
106695
106831
  * The UI view of a stored secret field value: a marker becomes "set", inline
106696
106832
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -107342,11 +107478,36 @@ function projectStore(store, perNodeKeys, nodeId) {
107342
107478
  }
107343
107479
  return out;
107344
107480
  }
107481
+ /**
107482
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
107483
+ *
107484
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
107485
+ * scope (global, one node, one device, one integration, one row of a business
107486
+ * collection) and the field path inside that scope — so saving the same field
107487
+ * twice updates the same ref, and the ref itself says which settings blob
107488
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
107489
+ * copied onto another ref does not decrypt.
107490
+ *
107491
+ * Array items are addressed by ITEM ID, never by index
107492
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
107493
+ *
107494
+ * Pure and browser-safe: no node builtins, no I/O.
107495
+ */
107496
+ /** The URI scheme of every keychain ref. */
107497
+ var SECRET_REF_SCHEME = "secret://";
107345
107498
  var GLOBAL_SCOPE = { kind: "global" };
107346
107499
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
107347
107500
  function globalSecretScope() {
107348
107501
  return GLOBAL_SCOPE;
107349
107502
  }
107503
+ /**
107504
+ * An addon id as a ref owner. A hub in-process builtin's context id is
107505
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
107506
+ * addon running forked.
107507
+ */
107508
+ function normalizeSecretOwner(addonId) {
107509
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
107510
+ }
107350
107511
  function encodeSegment(value) {
107351
107512
  return encodeURIComponent(value);
107352
107513
  }
@@ -107357,6 +107518,16 @@ function decodeSegment(value) {
107357
107518
  return null;
107358
107519
  }
107359
107520
  }
107521
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
107522
+ function formatSecretScope(scope) {
107523
+ switch (scope.kind) {
107524
+ case "global": return "global";
107525
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
107526
+ case "device": return `device/${scope.deviceId}`;
107527
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
107528
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
107529
+ }
107530
+ }
107360
107531
  /**
107361
107532
  * The field path of a value inside an array item, addressed by the item's id:
107362
107533
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -107365,6 +107536,13 @@ function decodeSegment(value) {
107365
107536
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
107366
107537
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
107367
107538
  }
107539
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
107540
+ function buildSecretRef(parts) {
107541
+ const owner = normalizeSecretOwner(parts.owner);
107542
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
107543
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
107544
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
107545
+ }
107368
107546
  function parseScope(segments) {
107369
107547
  switch (segments[0]) {
107370
107548
  case "global": return {
@@ -109277,6 +109455,45 @@ function normalizeAddonInitResult(result) {
109277
109455
  if (Array.isArray(result)) return { providers: result };
109278
109456
  return result;
109279
109457
  }
109458
+ var PREFIX = "records/";
109459
+ /** The base of every records failure. `detail` is the human part of the message. */
109460
+ var RecordsError = class extends Error {
109461
+ code;
109462
+ detail;
109463
+ constructor(code, detail) {
109464
+ super(`${PREFIX}${code}: ${detail}`);
109465
+ this.name = "RecordsError";
109466
+ this.code = code;
109467
+ this.detail = detail;
109468
+ }
109469
+ };
109470
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
109471
+ var RevConflictError = class extends RecordsError {
109472
+ constructor(detail) {
109473
+ super("rev-conflict", detail);
109474
+ this.name = "RevConflictError";
109475
+ }
109476
+ };
109477
+ /** The keychain could not store or serve a value: explicit, never an empty string. */
109478
+ var RecordSecretUnavailableError = class extends RecordsError {
109479
+ constructor(detail) {
109480
+ super("secret-unavailable", detail);
109481
+ this.name = "RecordSecretUnavailableError";
109482
+ }
109483
+ };
109484
+ var InvalidRecordError = class extends RecordsError {
109485
+ constructor(detail) {
109486
+ super("invalid-record", detail);
109487
+ this.name = "InvalidRecordError";
109488
+ }
109489
+ };
109490
+ /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
109491
+ var DeclarationRefusedError = class extends RecordsError {
109492
+ constructor(detail) {
109493
+ super("declaration-refused", detail);
109494
+ this.name = "DeclarationRefusedError";
109495
+ }
109496
+ };
109280
109497
  /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
109281
109498
  /** The files a kind may live in. `history` is device-history's (D850), never records. */
109282
109499
  var RECORD_DATABASES = ["settings", "events"];
@@ -109292,6 +109509,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
109292
109509
  var RECORD_MAX_DATA_BYTES = 1024 * 1024;
109293
109510
  var RECORD_BATCH_MAX = 1e3;
109294
109511
  var RECORD_QUERY_MAX = 1e3;
109512
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
109513
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
109514
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
109515
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
109516
+ /**
109517
+ * Secret fields of a record, addressed the way a Zod schema declares them
109518
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
109519
+ * (`auth.accessToken`, `headers[].value`).
109520
+ *
109521
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
109522
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
109523
+ * key releases exactly that key's secret.
109524
+ *
109525
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
109526
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
109527
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
109528
+ * leaf names its element: `headers[X-Api].value`.
109529
+ *
109530
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
109531
+ */
109532
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
109533
+ function parseSecretPath(path) {
109534
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
109535
+ return path.split(".").map((raw) => {
109536
+ const each = raw.endsWith("[]");
109537
+ const map = raw.endsWith("{}");
109538
+ const key = each || map ? raw.slice(0, -2) : raw;
109539
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
109540
+ return {
109541
+ key,
109542
+ each,
109543
+ map
109544
+ };
109545
+ });
109546
+ }
109547
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
109548
+ function secretArrayPrefixes(path) {
109549
+ const segments = parseSecretPath(path);
109550
+ const prefixes = [];
109551
+ const walked = [];
109552
+ for (const segment of segments) {
109553
+ walked.push(segment.key);
109554
+ if (segment.each) prefixes.push(walked.join("."));
109555
+ }
109556
+ return prefixes;
109557
+ }
109558
+ function isRecord$4(value) {
109559
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109560
+ }
109561
+ function elementId(element, idKey, declared, prefix) {
109562
+ const id = isRecord$4(element) ? element[idKey] : void 0;
109563
+ 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`);
109564
+ return id;
109565
+ }
109566
+ /**
109567
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
109568
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
109569
+ * result is a new body, `data` is untouched.
109570
+ */
109571
+ var REMOVE_LEAF = Symbol("remove-leaf");
109572
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
109573
+ if (!isRecord$4(node)) return node;
109574
+ const segment = segments[index];
109575
+ if (segment === void 0) return node;
109576
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
109577
+ const child = node[segment.key];
109578
+ const keys = [...walkedKeys, segment.key];
109579
+ const last = index === segments.length - 1;
109580
+ if (segment.each) {
109581
+ if (!Array.isArray(child)) return node;
109582
+ const prefix = keys.join(".");
109583
+ const idKey = arrayKeys[prefix];
109584
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
109585
+ const mapped = child.map((element) => {
109586
+ const id = elementId(element, idKey, declared, prefix);
109587
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
109588
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
109589
+ });
109590
+ return {
109591
+ ...node,
109592
+ [segment.key]: mapped
109593
+ };
109594
+ }
109595
+ if (segment.map) {
109596
+ if (!isRecord$4(child)) return node;
109597
+ const kept = Object.entries(child).map(([mapKey, value]) => {
109598
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
109599
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
109600
+ return [mapKey, visit({
109601
+ declared,
109602
+ concrete,
109603
+ value
109604
+ })];
109605
+ }).filter(([, value]) => value !== REMOVE_LEAF);
109606
+ return {
109607
+ ...node,
109608
+ [segment.key]: Object.fromEntries(kept)
109609
+ };
109610
+ }
109611
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
109612
+ if (last) {
109613
+ const replacement = visit({
109614
+ declared,
109615
+ concrete,
109616
+ value: child
109617
+ });
109618
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
109619
+ return {
109620
+ ...node,
109621
+ [segment.key]: replacement
109622
+ };
109623
+ }
109624
+ return {
109625
+ ...node,
109626
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
109627
+ };
109628
+ }
109629
+ /** Replace/remove every present secret leaf; returns a NEW body. */
109630
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
109631
+ let current = data;
109632
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
109633
+ return current;
109634
+ }
109635
+ /** Every present secret leaf, read-only. */
109636
+ function collectSecretLeaves(data, paths, arrayKeys) {
109637
+ const found = [];
109638
+ mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
109639
+ found.push(leaf);
109640
+ return leaf.value;
109641
+ });
109642
+ return found;
109643
+ }
109644
+ /**
109645
+ * The keychain collection name a kind's secrets live under. Prefixed so a
109646
+ * record's refs never collide with the refs a legacy collection of the same name
109647
+ * already holds during an adoption.
109648
+ */
109649
+ function recordSecretCollection(kind) {
109650
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
109651
+ }
109652
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
109653
+ function recordSecretRef(owner, kind, rowId, concretePath) {
109654
+ return buildSecretRef({
109655
+ owner,
109656
+ scope: {
109657
+ kind: "row",
109658
+ collection: recordSecretCollection(kind),
109659
+ rowId
109660
+ },
109661
+ fieldPath: concretePath
109662
+ });
109663
+ }
109664
+ /**
109665
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
109666
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
109667
+ * token. The form hands it to the secret field, which reveals the value by ref
109668
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
109669
+ * stored secret (the records door reads a view as KEEP).
109670
+ *
109671
+ * ONE implementation for the entries service and for every addon that serves
109672
+ * its own instances (`RecordIntegrationInstances`).
109673
+ */
109674
+ function isRecord$3(value) {
109675
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109676
+ }
109677
+ function secretViewsOf(owner, kind, id, decl, data) {
109678
+ if (decl.secretPaths.length === 0) return data;
109679
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
109680
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
109681
+ return {
109682
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
109683
+ v: 1,
109684
+ set: true
109685
+ };
109686
+ });
109687
+ return isRecord$3(shown) ? shown : data;
109688
+ }
109295
109689
  /**
109296
109690
  * The share-view scope kinds a share-link token can carry. The hub mints
109297
109691
  * and validates them (`ShareViewGrantSchema` builds its
@@ -109451,6 +109845,126 @@ var RecordKindDeclarationSchema = object({
109451
109845
  }).strict();
109452
109846
  /** The kind names: lower-case, dash separated, short enough for an index name. */
109453
109847
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
109848
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
109849
+ function assertValidDeclaration(name, decl) {
109850
+ const refuse = (why) => {
109851
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
109852
+ };
109853
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
109854
+ const seen = /* @__PURE__ */ new Set();
109855
+ for (const index of decl.indexes) {
109856
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
109857
+ seen.add(index.name);
109858
+ }
109859
+ const { retention } = decl;
109860
+ const hasSecrets = decl.secretPaths.length > 0;
109861
+ if ("ttlMs" in retention) {
109862
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
109863
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
109864
+ }
109865
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
109866
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
109867
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
109868
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
109869
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
109870
+ 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)`);
109871
+ }
109872
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
109873
+ 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`);
109874
+ }
109875
+ function bodyFieldsOf(schema) {
109876
+ return { ...toJSONSchema(schema, {
109877
+ unrepresentable: "any",
109878
+ io: "input"
109879
+ }) };
109880
+ }
109881
+ function isRecordLike(value) {
109882
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109883
+ }
109884
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
109885
+ function hasNameField(json, field) {
109886
+ const branches = json["oneOf"] ?? json["anyOf"];
109887
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
109888
+ const properties = json["properties"];
109889
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
109890
+ }
109891
+ /**
109892
+ * Declare the kinds of a package. The keys are the kind names. The result is
109893
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
109894
+ * types its handle from.
109895
+ */
109896
+ function defineRecordKinds(specs) {
109897
+ const out = {};
109898
+ for (const [name, spec] of Object.entries(specs)) {
109899
+ const secretPaths = collectZodSecretPaths(spec.schema);
109900
+ const nameField = spec.entry?.nameField ?? "name";
109901
+ const fields = bodyFieldsOf(spec.schema);
109902
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
109903
+ const parsed = RecordKindDeclarationSchema.parse({
109904
+ database: spec.database,
109905
+ class: spec.class,
109906
+ writeTrigger: spec.writeTrigger,
109907
+ device: spec.device,
109908
+ at: spec.at,
109909
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
109910
+ retention: spec.retention,
109911
+ deviceRemoval: spec.deviceRemoval,
109912
+ maxDataBytes: spec.maxDataBytes,
109913
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
109914
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
109915
+ transactFamily: spec.transactFamily,
109916
+ entry: spec.entry === void 0 ? void 0 : {
109917
+ title: spec.entry.title,
109918
+ nameField,
109919
+ fields
109920
+ }
109921
+ });
109922
+ assertValidDeclaration(name, parsed);
109923
+ out[name] = {
109924
+ name,
109925
+ schema: spec.schema,
109926
+ declaration: parsed
109927
+ };
109928
+ }
109929
+ return out;
109930
+ }
109931
+ /**
109932
+ * Entry kinds (D881): every list of user-created entries (notification
109933
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
109934
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
109935
+ * through the `record-entries` capability with password fields derived from
109936
+ * the schema (`.meta({ secret: true })`).
109937
+ */
109938
+ /** The body of a plain entry: a name and the fields the kind adds. */
109939
+ function entryBody(shape) {
109940
+ return object({
109941
+ name: string().min(1),
109942
+ ...shape
109943
+ });
109944
+ }
109945
+ /**
109946
+ * Declare one entry kind. An entry is configuration: never trimmed by the
109947
+ * engine, never aged, belonging to no device.
109948
+ */
109949
+ function defineEntryKind(name, spec) {
109950
+ const defined = defineRecordKinds({ [name]: {
109951
+ schema: spec.schema,
109952
+ class: "config",
109953
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
109954
+ retention: { none: "an entry goes only when the operator deletes it" },
109955
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
109956
+ ...spec.database === void 0 ? {} : { database: spec.database },
109957
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
109958
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
109959
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
109960
+ entry: {
109961
+ title: spec.title,
109962
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
109963
+ }
109964
+ } })[name];
109965
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
109966
+ return defined;
109967
+ }
109454
109968
  object({
109455
109969
  field: string().min(1),
109456
109970
  on: literal("advance")
@@ -113623,6 +114137,205 @@ var IntegrationInstanceRefSchema = object({
113623
114137
  ]),
113624
114138
  candidateStableId: string().optional()
113625
114139
  });
114140
+ /**
114141
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
114142
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
114143
+ *
114144
+ * Every instance is one record of the addon's own kind, keyed by the
114145
+ * `integrations` registry id. The records door does the secret work: a field the
114146
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
114147
+ * deleting the row deletes the secret, and one row's value is never another's.
114148
+ *
114149
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
114150
+ * token or the view the form was given KEEP the stored secret;
114151
+ * `{ $secretClear: true }` clears it; any other string replaces it.
114152
+ */
114153
+ /** The most instances one addon lists (an integrations page, not a table). */
114154
+ var INSTANCES_LIST_MAX = 1e3;
114155
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
114156
+ function topLevelSecretKeys(paths) {
114157
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
114158
+ }
114159
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
114160
+ function secretMapKeys(paths) {
114161
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
114162
+ }
114163
+ /**
114164
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
114165
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
114166
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
114167
+ */
114168
+ function secretMapPatch(value) {
114169
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
114170
+ const out = {};
114171
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
114172
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
114173
+ return Object.keys(out).length > 0 ? out : null;
114174
+ }
114175
+ /** Does the kind's body schema carry an `enabled` field? */
114176
+ function declaresEnabled(kind) {
114177
+ const json = toJSONSchema(kind.schema, {
114178
+ unrepresentable: "any",
114179
+ io: "input"
114180
+ });
114181
+ const properties = Reflect.get(json, "properties");
114182
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
114183
+ }
114184
+ var RecordIntegrationInstances = class {
114185
+ records;
114186
+ kind;
114187
+ hooks;
114188
+ handle;
114189
+ secretKeys;
114190
+ secretMaps;
114191
+ nameField;
114192
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
114193
+ registryOwnsEnabled;
114194
+ constructor(records, kind, hooks) {
114195
+ this.records = records;
114196
+ this.kind = kind;
114197
+ this.hooks = hooks;
114198
+ this.handle = records.kind(kind);
114199
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
114200
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
114201
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
114202
+ this.registryOwnsEnabled = hooks.description.role === "devices";
114203
+ 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"`);
114204
+ }
114205
+ /**
114206
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
114207
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
114208
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
114209
+ */
114210
+ async readResolved(id) {
114211
+ try {
114212
+ const record = await this.handle.getResolved(id);
114213
+ return record === null ? null : {
114214
+ id,
114215
+ body: record.data,
114216
+ enabled: record.data.enabled !== false,
114217
+ secrets: "resolved"
114218
+ };
114219
+ } catch (err) {
114220
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
114221
+ const stored = await this.handle.get(id);
114222
+ if (stored === null) return null;
114223
+ return {
114224
+ id,
114225
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
114226
+ enabled: stored.data.enabled !== false,
114227
+ secrets: "unavailable"
114228
+ };
114229
+ }
114230
+ }
114231
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
114232
+ async getResolved(id) {
114233
+ return (await this.readResolved(id))?.body ?? null;
114234
+ }
114235
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
114236
+ async listResolved() {
114237
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
114238
+ const out = [];
114239
+ for (const record of page.records) {
114240
+ const read = await this.readResolved(record.id);
114241
+ if (read !== null) out.push(read);
114242
+ }
114243
+ return out;
114244
+ }
114245
+ viewOf(record) {
114246
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
114247
+ const rawName = record.data[this.nameField];
114248
+ const kind = this.hooks.kindOf?.(record.data);
114249
+ const health = this.hooks.health?.(record.data, record.id);
114250
+ return {
114251
+ id: record.id,
114252
+ name: typeof rawName === "string" ? rawName : record.id,
114253
+ configured: this.hooks.configured(record.data),
114254
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
114255
+ formValues: shown,
114256
+ ...kind !== void 0 ? { kind } : {},
114257
+ ...health?.status !== void 0 ? { status: health.status } : {},
114258
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
114259
+ };
114260
+ }
114261
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
114262
+ ...this.hooks.description,
114263
+ mintsId: true
114264
+ };
114265
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
114266
+ listInstances = async () => {
114267
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
114268
+ };
114269
+ getInstance = async ({ id }) => {
114270
+ const record = await this.handle.get(id);
114271
+ return record === null ? null : this.viewOf(record);
114272
+ };
114273
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
114274
+ createInstance = async ({ id, name, kind }) => {
114275
+ const minted = id ?? this.hooks.mintId?.({
114276
+ name,
114277
+ ...kind !== void 0 ? { kind } : {}
114278
+ });
114279
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
114280
+ await this.handle.put({
114281
+ id: minted,
114282
+ data: this.hooks.initialBody(name, kind)
114283
+ }, { ifRev: 0 });
114284
+ await this.hooks.onChanged?.(minted, "created");
114285
+ return { id: minted };
114286
+ };
114287
+ updateInstance = async ({ id, name, values }) => {
114288
+ const patch = {};
114289
+ for (const [key, value] of Object.entries(values)) {
114290
+ if (value === void 0) continue;
114291
+ if (this.secretMaps.has(key)) {
114292
+ const entries = secretMapPatch(value);
114293
+ if (entries !== null) patch[key] = entries;
114294
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
114295
+ else if (isSecretClear(value)) patch[key] = null;
114296
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
114297
+ }
114298
+ if (name !== void 0) patch[this.nameField] = name;
114299
+ await this.handle.patch(id, patch);
114300
+ await this.hooks.onChanged?.(id, "updated");
114301
+ return { ok: true };
114302
+ };
114303
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
114304
+ setInstanceEnabled = async ({ id, enabled }) => {
114305
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
114306
+ await this.handle.patch(id, { enabled });
114307
+ await this.hooks.onChanged?.(id, "enabled");
114308
+ return { ok: true };
114309
+ };
114310
+ /** The row and its keychain refs go together (the records door). */
114311
+ deleteInstance = async ({ id }) => {
114312
+ await this.handle.delete(id);
114313
+ await this.hooks.onChanged?.(id, "deleted");
114314
+ return { ok: true };
114315
+ };
114316
+ testInstance = async (input) => {
114317
+ if (this.hooks.test === void 0) return {
114318
+ success: false,
114319
+ status: "unsupported",
114320
+ error: "This integration declares no connection test"
114321
+ };
114322
+ return this.hooks.test(input);
114323
+ };
114324
+ /** The flat provider object to hand to `registerProvider`. */
114325
+ toProvider() {
114326
+ return {
114327
+ describe: this.describe,
114328
+ instanceFormSchema: this.instanceFormSchema,
114329
+ listInstances: this.listInstances,
114330
+ getInstance: this.getInstance,
114331
+ createInstance: this.createInstance,
114332
+ updateInstance: this.updateInstance,
114333
+ setInstanceEnabled: this.setInstanceEnabled,
114334
+ deleteInstance: this.deleteInstance,
114335
+ testInstance: this.testInstance
114336
+ };
114337
+ }
114338
+ };
113626
114339
  var EncodeProfileSchema = object({
113627
114340
  video: object({
113628
114341
  codec: _enum([
@@ -114727,6 +115440,130 @@ var MigrationOwnerStepSchema = object({
114727
115440
  /** The ledger state the engine found: `pending` has no row yet. */
114728
115441
  state: union([literal("pending"), MigrationStatusSchema])
114729
115442
  });
115443
+ /**
115444
+ * Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
115445
+ * kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
115446
+ * the integration id, the devices stamped with it and the automations that name it do not change.
115447
+ *
115448
+ * Order, so a crash or a locked keychain is safe at every point:
115449
+ *
115450
+ * 1. Phase A, read only: every row of every integration of the addon is read with its secrets
115451
+ * resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
115452
+ * anything is written; `block-owner` then holds the addon.
115453
+ * 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
115454
+ * `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
115455
+ * and every secret leaf must equal what the rows held, else the record is deleted and the pass
115456
+ * throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
115457
+ * a warning names the path. Only then are the rows (and their keychain values) cleared.
115458
+ *
115459
+ * Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
115460
+ * a no-op. A default integration with no rows still gets its record (an empty body, D985).
115461
+ * Never logs or throws a secret value; messages name integration ids and secret PATHS only.
115462
+ */
115463
+ function secretLeavesOf(body, spec) {
115464
+ const out = /* @__PURE__ */ new Map();
115465
+ 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);
115466
+ return out;
115467
+ }
115468
+ function why(err) {
115469
+ return err instanceof Error ? err.message : String(err);
115470
+ }
115471
+ async function moveIntegrationRowsToRecords(ports, spec) {
115472
+ const integrations = await ports.listIntegrations(spec.addonId);
115473
+ if (integrations.length === 0) return {
115474
+ outcome: "already-done",
115475
+ summary: { integrations: 0 }
115476
+ };
115477
+ const rowsOf = /* @__PURE__ */ new Map();
115478
+ for (const integration of integrations) try {
115479
+ rowsOf.set(integration.id, await ports.readRows(integration.id));
115480
+ } catch (err) {
115481
+ throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
115482
+ }
115483
+ let created = 0;
115484
+ let cleared = 0;
115485
+ let kept = 0;
115486
+ for (const integration of integrations) {
115487
+ const values = rowsOf.get(integration.id) ?? {};
115488
+ const hasRows = Object.keys(values).length > 0;
115489
+ const body = spec.bodyFrom(values, ports, integration);
115490
+ const put = await ports.putRecord(integration.id, body);
115491
+ if (put === "created") created += 1;
115492
+ if (!hasRows) continue;
115493
+ const wanted = secretLeavesOf(body, spec);
115494
+ const back = await ports.readRecord(integration.id);
115495
+ if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
115496
+ const held = secretLeavesOf(back, spec);
115497
+ if (put === "created") {
115498
+ const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
115499
+ if (wrong.length > 0) {
115500
+ await ports.deleteRecord(integration.id);
115501
+ 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`);
115502
+ }
115503
+ } else {
115504
+ const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
115505
+ if (missing.length > 0) {
115506
+ ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
115507
+ kept += 1;
115508
+ continue;
115509
+ }
115510
+ }
115511
+ await ports.clearRows(integration.id);
115512
+ cleared += 1;
115513
+ }
115514
+ return created === 0 && cleared === 0 ? {
115515
+ outcome: "already-done",
115516
+ summary: {
115517
+ integrations: integrations.length,
115518
+ kept
115519
+ }
115520
+ } : {
115521
+ outcome: "done",
115522
+ summary: {
115523
+ integrations: integrations.length,
115524
+ created,
115525
+ cleared,
115526
+ kept
115527
+ }
115528
+ };
115529
+ }
115530
+ /**
115531
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
115532
+ * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
115533
+ * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
115534
+ * the new record through the addon's own records handle. The hub deletes the rows and their
115535
+ * keychain values; the addon never touches the registry.
115536
+ */
115537
+ function integrationRowsPortsOverApi(deps) {
115538
+ return {
115539
+ warn: (message) => deps.log.warn(message),
115540
+ listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
115541
+ id: i.id,
115542
+ name: i.name
115543
+ })),
115544
+ readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
115545
+ putRecord: async (id, body) => {
115546
+ if (await deps.records.get(id) !== null) return "exists";
115547
+ try {
115548
+ await deps.records.put({
115549
+ id,
115550
+ data: body
115551
+ }, { ifRev: 0 });
115552
+ } catch (err) {
115553
+ if (err instanceof RevConflictError) return "exists";
115554
+ throw err;
115555
+ }
115556
+ return "created";
115557
+ },
115558
+ readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
115559
+ deleteRecord: async (id) => {
115560
+ await deps.records.delete(id);
115561
+ },
115562
+ clearRows: async (integrationId) => {
115563
+ await deps.api.clearSettings({ id: integrationId });
115564
+ }
115565
+ };
115566
+ }
114730
115567
  function ordinalOf(id) {
114731
115568
  const digits = id.replace(/\D+/g, "");
114732
115569
  return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
@@ -114801,6 +115638,40 @@ async function claimDevicesToDefaultIntegration(ports, spec) {
114801
115638
  }
114802
115639
  };
114803
115640
  }
115641
+ /**
115642
+ * The two migrations of a device addon whose integrations hold NO settings of their own
115643
+ * (planning#416, S-A): every integration becomes a record of the addon's own kind with a name and
115644
+ * nothing else (D985, the default integration is an instance with an empty body), then the devices
115645
+ * that carry no `integrationId` are claimed to the addon's default integration.
115646
+ *
115647
+ * 1. `rows-to-record` (block-owner): `moveIntegrationRowsToRecords` over the hub API. Rows an
115648
+ * addon of this shape does not own (a key nothing reads) are named in a warning, by key, and
115649
+ * dropped with the rows once the record is read back.
115650
+ * 2. `claim-devices` (tolerate): `claimDevicesToDefaultIntegration`; with no integration to claim
115651
+ * to it fails and is retried on the next boot.
115652
+ *
115653
+ * Both run in the addon process through the hub API (`ctx.api`), BEFORE the addon registers its
115654
+ * `integration-instances` provider: while a provider is registered the hub answers `getSettings`
115655
+ * from the record, not from the rows.
115656
+ */
115657
+ function defaultIntegrationPortsOver(deps) {
115658
+ return {
115659
+ ...integrationRowsPortsOverApi({
115660
+ api: deps.api,
115661
+ records: deps.records.kind({
115662
+ name: deps.kind.name,
115663
+ declaration: deps.kind.declaration,
115664
+ schema: record(string(), unknown())
115665
+ }),
115666
+ log: { warn: deps.warn }
115667
+ }),
115668
+ ...claimDevicesPortsOver({
115669
+ api: deps.api,
115670
+ info: deps.info,
115671
+ warnDevice: deps.warnDevice
115672
+ })
115673
+ };
115674
+ }
114804
115675
  /** The hub side of the device claim, over `ctx.api`. */
114805
115676
  function claimDevicesPortsOver(deps) {
114806
115677
  return {
@@ -157066,32 +157937,57 @@ var IntegrationInstancePatchSchema = object({
157066
157937
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
157067
157938
  values: record(string(), unknown())
157068
157939
  });
157069
- 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({
157070
- id: IntegrationInstanceIdSchema.optional(),
157071
- name: string().min(1),
157072
- kind: string().min(1).optional()
157073
- }), object({ id: string() }), {
157074
- kind: "mutation",
157075
- auth: "admin"
157076
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
157077
- kind: "mutation",
157078
- auth: "admin"
157079
- }), method(object({
157080
- id: IntegrationInstanceIdSchema,
157081
- enabled: boolean()
157082
- }), object({ ok: literal(true) }), {
157083
- kind: "mutation",
157084
- auth: "admin"
157085
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
157086
- kind: "mutation",
157087
- auth: "admin"
157088
- }), optionalMethod(object({
157089
- id: IntegrationInstanceIdSchema.nullable(),
157090
- values: record(string(), unknown())
157091
- }), TestConnectionResultSchema$1, {
157092
- kind: "mutation",
157093
- auth: "admin"
157094
- });
157940
+ var integrationInstancesCapability = {
157941
+ name: "integration-instances",
157942
+ scope: "system",
157943
+ mode: "collection",
157944
+ methods: {
157945
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
157946
+ /** The form; `id: null` is the create form. */
157947
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
157948
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
157949
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
157950
+ /**
157951
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
157952
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
157953
+ */
157954
+ createInstance: method(object({
157955
+ id: IntegrationInstanceIdSchema.optional(),
157956
+ name: string().min(1),
157957
+ kind: string().min(1).optional()
157958
+ }), object({ id: string() }), {
157959
+ kind: "mutation",
157960
+ auth: "admin"
157961
+ }),
157962
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
157963
+ kind: "mutation",
157964
+ auth: "admin"
157965
+ }),
157966
+ /**
157967
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
157968
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
157969
+ */
157970
+ setInstanceEnabled: method(object({
157971
+ id: IntegrationInstanceIdSchema,
157972
+ enabled: boolean()
157973
+ }), object({ ok: literal(true) }), {
157974
+ kind: "mutation",
157975
+ auth: "admin"
157976
+ }),
157977
+ /** Delete the record and its keychain refs together. */
157978
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
157979
+ kind: "mutation",
157980
+ auth: "admin"
157981
+ }),
157982
+ testInstance: optionalMethod(object({
157983
+ id: IntegrationInstanceIdSchema.nullable(),
157984
+ values: record(string(), unknown())
157985
+ }), TestConnectionResultSchema$1, {
157986
+ kind: "mutation",
157987
+ auth: "admin"
157988
+ })
157989
+ }
157990
+ };
157095
157991
  /**
157096
157992
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
157097
157993
  * session (server-initiated offer pattern, identical to the main
@@ -220342,7 +221238,7 @@ function text(stored, key) {
220342
221238
  const value = stored[key];
220343
221239
  return typeof value === "string" && value.length > 0 ? value : null;
220344
221240
  }
220345
- function portOf(stored, ports) {
221241
+ function portOf$1(stored, ports) {
220346
221242
  const value = stored["emailPushPort"];
220347
221243
  const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
220348
221244
  if (Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535) return numeric;
@@ -220364,7 +221260,7 @@ async function moveEmailPushToMailbox(ports) {
220364
221260
  };
220365
221261
  const bindHost = text(stored, "emailPushBindHost");
220366
221262
  await ports.stashPending({
220367
- port: portOf(stored, ports),
221263
+ port: portOf$1(stored, ports),
220368
221264
  ...bindHost === null ? {} : { bindHost },
220369
221265
  domain: text(stored, "emailPushDomain") ?? DEFAULT_DOMAIN,
220370
221266
  requireAuth: stored["emailPushRequireAuth"] !== false,
@@ -220376,6 +221272,188 @@ async function moveEmailPushToMailbox(ports) {
220376
221272
  summary: { moved: true }
220377
221273
  };
220378
221274
  }
221275
+ var reolinkConnectionsEntryKind = defineEntryKind("connections", {
221276
+ title: "Reolink integrations",
221277
+ schema: entryBody({
221278
+ host: string().nullish(),
221279
+ port: number().int().min(1).max(65535).nullish(),
221280
+ rtspPort: number().int().min(1).max(65535).nullish(),
221281
+ username: string().nullish(),
221282
+ password: string().optional().meta({ secret: true }),
221283
+ transport: _enum(["tcp", "udp"]).nullish(),
221284
+ uid: string().nullish(),
221285
+ debugGeneral: boolean().nullish(),
221286
+ debugSocketLogs: array(ReolinkSocketDebugFlagSchema).nullish()
221287
+ }),
221288
+ writeTrigger: "an operator adds, edits or removes a Reolink integration, or a Hub device is migrated into one"
221289
+ });
221290
+ /** Every kind this addon declares, keyed by its name. */
221291
+ var reolinkRecordKinds = { connections: reolinkConnectionsEntryKind };
221292
+ /** The record body as the connection form's flat values: no name, no unset keys. */
221293
+ function hubSettingsOf(body) {
221294
+ const out = {};
221295
+ for (const [key, value] of Object.entries(body)) {
221296
+ if (key === "name" || value === null || value === void 0) continue;
221297
+ out[key] = value;
221298
+ }
221299
+ return out;
221300
+ }
221301
+ /**
221302
+ * What a resolved record says about its hub. A keychain that is locked yields a record WITHOUT
221303
+ * its password: that is `password-locked`, never an empty password (D49).
221304
+ */
221305
+ function hubConnectionOf(resolved) {
221306
+ const read = readHubConnection(hubSettingsOf(resolved.body));
221307
+ return read.kind === "no-password" && resolved.secrets === "unavailable" ? { kind: "password-locked" } : read;
221308
+ }
221309
+ /**
221310
+ * What each enabled integration's RECORD says about its hub: the one reader of the connection.
221311
+ * A record that is absent reads as `no-host` (no session), exactly as an empty settings blob did.
221312
+ */
221313
+ async function readHubConnections(instances, integrations) {
221314
+ const out = [];
221315
+ for (const integration of integrations) {
221316
+ const resolved = await instances.readResolved(integration.id);
221317
+ out.push({
221318
+ id: integration.id,
221319
+ name: integration.name,
221320
+ read: resolved === null ? { kind: "no-host" } : hubConnectionOf(resolved)
221321
+ });
221322
+ }
221323
+ return out;
221324
+ }
221325
+ function buildReolinkInstances(records, deps) {
221326
+ return new RecordIntegrationInstances(records, reolinkConnectionsEntryKind, {
221327
+ description: {
221328
+ role: "devices",
221329
+ discovery: {
221330
+ mode: "lan",
221331
+ scopedToInstance: false
221332
+ },
221333
+ instanceMode: "multiple",
221334
+ kind: "connection",
221335
+ canTest: false
221336
+ },
221337
+ formSchema: () => buildHubInstanceSchema(),
221338
+ initialBody: (name) => ({ name }),
221339
+ configured: (body) => (body.host ?? "").length === 0 || (body.password ?? "").length > 0,
221340
+ onChanged: () => deps.onChanged()
221341
+ });
221342
+ }
221343
+ /**
221344
+ * The provider handed to the hub: the default integration (no connection) shows no hub form, an
221345
+ * NVR / Home Hub one does, and a create wizard (no id yet) sets up a hub.
221346
+ */
221347
+ function reolinkInstancesProvider(instances, deps) {
221348
+ return {
221349
+ ...instances.toProvider(),
221350
+ instanceFormSchema: async ({ id }) => {
221351
+ if (id === null) return buildHubInstanceSchema();
221352
+ const [evidence, body] = await Promise.all([deps.registryEvidenceOf(id), instances.getResolved(id)]);
221353
+ return evidence !== null && isHubIntegration({
221354
+ originStableId: evidence.originStableId,
221355
+ info: evidence.info,
221356
+ settings: body === null ? {} : hubSettingsOf(body)
221357
+ }) || body === null ? buildHubInstanceSchema() : { sections: [] };
221358
+ }
221359
+ };
221360
+ }
221361
+ //#endregion
221362
+ //#region src/migrations/integration-rows-to-record.ts
221363
+ /**
221364
+ * Every Reolink integration moves from legacy `integration_settings` rows into its own
221365
+ * `connections` record under the registry id (planning#416, D1007). An NVR / Home Hub integration
221366
+ * carries its connection (host, ports, username, transport, uid, the two debug fields) and its
221367
+ * password, a keychain secret declared by the kind; the default integration carries a name only.
221368
+ * Each integration's record is built from ITS OWN rows, never from another's.
221369
+ *
221370
+ * Keys nothing reads any more are named in a warning, by key, and dropped with the rows once the
221371
+ * record has been read back. A value of the wrong type is left out and named by key as well.
221372
+ */
221373
+ var REOLINK_ROWS_TO_RECORD_ID = "provider-reolink/2026-11-05-integration-rows-to-record";
221374
+ var TEXT_KEYS = [
221375
+ "host",
221376
+ "username",
221377
+ "uid"
221378
+ ];
221379
+ var PORT_KEYS = ["port", "rtspPort"];
221380
+ function portOf(value) {
221381
+ const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
221382
+ return typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
221383
+ }
221384
+ function textOf(value) {
221385
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
221386
+ }
221387
+ /** The record body of one integration, from its resolved legacy rows. Never logs a value. */
221388
+ function reolinkBodyFromRows(values, log, integration) {
221389
+ const body = { name: integration.name };
221390
+ const unusable = [];
221391
+ const known = new Set([
221392
+ ...TEXT_KEYS,
221393
+ ...PORT_KEYS,
221394
+ "password",
221395
+ "transport",
221396
+ "debugGeneral",
221397
+ "debugSocketLogs"
221398
+ ]);
221399
+ for (const key of TEXT_KEYS) {
221400
+ if (!(key in values)) continue;
221401
+ const text = textOf(values[key]);
221402
+ if (text === null) unusable.push(key);
221403
+ else body[key] = text;
221404
+ }
221405
+ for (const key of PORT_KEYS) {
221406
+ if (!(key in values)) continue;
221407
+ const port = portOf(values[key]);
221408
+ if (port === null) unusable.push(key);
221409
+ else body[key] = port;
221410
+ }
221411
+ if ("password" in values) {
221412
+ const password = values["password"];
221413
+ if (typeof password === "string" && password.length > 0) body["password"] = password;
221414
+ else unusable.push("password");
221415
+ }
221416
+ if ("transport" in values) {
221417
+ const transport = values["transport"];
221418
+ if (transport === "tcp" || transport === "udp") body["transport"] = transport;
221419
+ else unusable.push("transport");
221420
+ }
221421
+ if ("debugGeneral" in values) {
221422
+ const flag = values["debugGeneral"];
221423
+ if (typeof flag === "boolean") body["debugGeneral"] = flag;
221424
+ else unusable.push("debugGeneral");
221425
+ }
221426
+ if ("debugSocketLogs" in values) {
221427
+ const flags = values["debugSocketLogs"];
221428
+ const parsed = Array.isArray(flags) ? flags.map((f) => ReolinkSocketDebugFlagSchema.safeParse(f)) : null;
221429
+ if (parsed !== null && parsed.every((p) => p.success)) body["debugSocketLogs"] = parsed.flatMap((p) => p.success ? [p.data] : []);
221430
+ else unusable.push("debugSocketLogs");
221431
+ }
221432
+ const unknown = Object.keys(values).filter((key) => !known.has(key));
221433
+ if (unknown.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${unknown.join(", ")}`);
221434
+ if (unusable.length > 0) log.warn(`integration '${integration.id}': legacy settings with an unusable value are left out: ${unusable.join(", ")}`);
221435
+ return reolinkConnectionsEntryKind.schema.parse(body);
221436
+ }
221437
+ var REOLINK_ROWS_TO_RECORD_MIGRATION = {
221438
+ meta: {
221439
+ id: REOLINK_ROWS_TO_RECORD_ID,
221440
+ owner: "provider-reolink",
221441
+ level: "addon-state",
221442
+ seq: 4,
221443
+ dependsOn: [],
221444
+ store: "settings",
221445
+ scope: "cluster",
221446
+ onFailure: "block-owner",
221447
+ bodyKind: "procedure",
221448
+ 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",
221449
+ fixtureMark: "a legacy setting key the migration does not know"
221450
+ },
221451
+ procedure: (ports) => moveIntegrationRowsToRecords(ports, {
221452
+ addonId: "provider-reolink",
221453
+ secretPaths: reolinkConnectionsEntryKind.declaration.secretPaths,
221454
+ bodyFrom: reolinkBodyFromRows
221455
+ })
221456
+ };
220379
221457
  /** The `provider-reolink` catalogue (D987): offered to the engine when the addon initialises. */
220380
221458
  var REOLINK_MIGRATIONS = [{
220381
221459
  meta: {
@@ -220409,17 +221487,17 @@ var REOLINK_MIGRATIONS = [{
220409
221487
  plan: planDropWebhookIntakeSettings
220410
221488
  }];
220411
221489
  /**
220412
- * Every persisted camera that names no integration is claimed to the addon's default one. The
220413
- * integration a hub migration created is never the default, so it is not eligible. The integration
220414
- * settings themselves stay where they are until this addon's own wave moves them.
221490
+ * The integration wave (D1007) in declaration order, which must follow `seq`: the device claim
221491
+ * shipped first (seq 3) and the rows move follows it (seq 4). The engine refuses the whole
221492
+ * catalogue when a seq does not increase, and the addon then does not start.
220415
221493
  */
220416
- var REOLINK_CLAIM_MIGRATION = claimDevicesMigration({
221494
+ var REOLINK_INTEGRATION_MIGRATIONS = [claimDevicesMigration({
220417
221495
  addonId: "provider-reolink",
220418
221496
  date: "2026-11-04",
220419
221497
  seq: 3,
220420
221498
  dependsOn: [],
220421
221499
  eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
220422
- });
221500
+ }), REOLINK_ROWS_TO_RECORD_MIGRATION];
220423
221501
  //#endregion
220424
221502
  //#region src/reolink-adoption-provider.ts
220425
221503
  var DEVICES_FILTER = {
@@ -220689,7 +221767,7 @@ var ReolinkHubManager = class {
220689
221767
  const { logger } = this.#deps;
220690
221768
  const wanted = /* @__PURE__ */ new Set();
220691
221769
  for (const integration of integrations) {
220692
- const read = readHubConnection(integration.settings);
221770
+ const { read } = integration;
220693
221771
  if (read.kind === "no-host") continue;
220694
221772
  wanted.add(integration.id);
220695
221773
  if (read.kind !== "ok") {
@@ -221057,6 +222135,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221057
222135
  */
221058
222136
  autodetectCache = new AutodetectCache();
221059
222137
  hubManager = null;
222138
+ instances = null;
221060
222139
  /**
221061
222140
  * The camera mailbox bridge (`email-push-bridge.ts`, D999): battery cameras mail motion to a
221062
222141
  * mailbox of the `smtp-inbound` broker, the bridge turns each message into the camera's
@@ -221302,25 +222381,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221302
222381
  }
221303
222382
  /**
221304
222383
  * Make the hub sessions match the live integration list: one per enabled integration with a
221305
- * host. Idempotent; runs at boot, after a migration and on every lifecycle event. When any
221306
- * integration's settings cannot be read the whole pass is skipped — an unreadable list must
222384
+ * host. Idempotent; runs at boot, after a migration, after an instance change and on every
222385
+ * lifecycle event. When any integration's record cannot be read the whole pass is skipped — an unreadable list must
221307
222386
  * never close a healthy session.
221308
222387
  */
221309
222388
  async reconcileIntegrations() {
221310
222389
  const manager = this.hubManager;
221311
- if (manager === null) return;
222390
+ const instances = this.instances;
222391
+ if (manager === null || instances === null) return;
221312
222392
  try {
221313
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
221314
- const withSettings = [];
221315
- for (const integration of mine) {
221316
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
221317
- withSettings.push({
221318
- id: integration.id,
221319
- name: integration.name,
221320
- settings
221321
- });
221322
- }
221323
- await manager.reconcile(withSettings);
222393
+ const withConnection = await readHubConnections(instances, (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled));
222394
+ await manager.reconcile(withConnection);
221324
222395
  } catch (err) {
221325
222396
  this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$3(err) } });
221326
222397
  }
@@ -221493,28 +222564,6 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221493
222564
  globalSettingsSchema() {
221494
222565
  return { sections: [...buildClipRefreshSettingsSchema().sections] };
221495
222566
  }
221496
- /**
221497
- * Per-instance form (D985): an NVR / Home Hub integration shows its connection only; the
221498
- * default integration keeps the full schema (`null`). A create wizard has no id yet and is
221499
- * always setting up a NEW instance — the default one already exists — so it gets the hub form.
221500
- */
221501
- async integrationSettingsSchema(instance) {
221502
- if (instance.integrationId === null) return buildHubInstanceSchema();
221503
- try {
221504
- const [integration, settings] = await Promise.all([this.ctx.api.integrations.get.query({ id: instance.integrationId }), this.ctx.api.integrations.getSettings.query({ id: instance.integrationId })]);
221505
- return isHubIntegration({
221506
- originStableId: integration.originStableId,
221507
- info: integration.info,
221508
- settings
221509
- }) ? buildHubInstanceSchema() : null;
221510
- } catch (err) {
221511
- this.ctx.logger.warn("Reolink integration form: instance lookup failed — full schema shown", { meta: {
221512
- integrationId: instance.integrationId,
221513
- error: errMsg$3(err)
221514
- } });
221515
- return null;
221516
- }
221517
- }
221518
222567
  async onInitialize() {
221519
222568
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
221520
222569
  this.logChannels = createLogChannelsProvider(this.ctx.logger.child("log-channels"));
@@ -221559,11 +222608,38 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221559
222608
  this.wireClipCatalogRefreshers();
221560
222609
  await this.serveClipPlanes(this.clipService);
221561
222610
  await this.serveWebhookPlane();
221562
- await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations([REOLINK_CLAIM_MIGRATION], claimDevicesPortsOver({
222611
+ const records = this.ctx.records;
222612
+ if (records === void 0) throw new Error("provider-reolink: ctx.records is required (hub-only addon)");
222613
+ await records.declare(reolinkRecordKinds);
222614
+ const integrationPorts = defaultIntegrationPortsOver({
221563
222615
  api: defaultIntegrationApiOver(this.ctx.api),
222616
+ records,
222617
+ kind: reolinkConnectionsEntryKind,
221564
222618
  info: (message) => this.ctx.logger.info(message),
222619
+ warn: (message) => this.ctx.logger.warn(message),
221565
222620
  warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
221566
- }))]);
222621
+ });
222622
+ const registryEvidenceOf = async (id) => {
222623
+ try {
222624
+ const integration = await this.ctx.api.integrations.get.query({ id });
222625
+ return {
222626
+ originStableId: integration.originStableId,
222627
+ info: { ...integration.info }
222628
+ };
222629
+ } catch (err) {
222630
+ this.ctx.logger.warn("Reolink integration form: instance lookup failed — hub form shown", { meta: {
222631
+ integrationId: id,
222632
+ error: errMsg$3(err)
222633
+ } });
222634
+ return null;
222635
+ }
222636
+ };
222637
+ const instances = buildReolinkInstances(records, {
222638
+ onChanged: () => this.reconcileIntegrations(),
222639
+ registryEvidenceOf
222640
+ });
222641
+ this.instances = instances;
222642
+ await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations(REOLINK_INTEGRATION_MIGRATIONS, integrationPorts)]);
221567
222643
  this.wireEmailBridge();
221568
222644
  const wireSleepPushResolvers = (event, via) => {
221569
222645
  const data = event.data;
@@ -221602,7 +222678,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221602
222678
  capability: deviceRemovalHookCapability,
221603
222679
  provider: createEmailPushRemovalHook({ resync: () => this.ensureEmailBridge().sync() })
221604
222680
  });
221605
- return regs;
222681
+ regs.push({
222682
+ capability: integrationInstancesCapability,
222683
+ provider: reolinkInstancesProvider(instances, { registryEvidenceOf })
222684
+ });
222685
+ return { providers: regs };
221606
222686
  }
221607
222687
  /** What the catalogue's migration reads and writes (D987). */
221608
222688
  migrationPorts() {