@camstack/addon-provider-velux 0.2.157 → 0.2.158

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
@@ -4594,6 +4594,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
4594
4594
  var booleanProcessor = (_schema, _ctx, json, _params) => {
4595
4595
  json.type = "boolean";
4596
4596
  };
4597
+ var bigintProcessor = (_schema, ctx, _json, _params) => {
4598
+ if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
4599
+ };
4600
+ var symbolProcessor = (_schema, ctx, _json, _params) => {
4601
+ if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
4602
+ };
4597
4603
  var nullProcessor = (_schema, ctx, json, _params) => {
4598
4604
  if (ctx.target === "openapi-3.0") {
4599
4605
  json.type = "string";
@@ -4601,12 +4607,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
4601
4607
  json.enum = [null];
4602
4608
  } else json.type = "null";
4603
4609
  };
4610
+ var undefinedProcessor = (_schema, ctx, _json, _params) => {
4611
+ if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
4612
+ };
4604
4613
  var voidProcessor = (_schema, ctx, _json, _params) => {
4605
4614
  if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
4606
4615
  };
4607
4616
  var neverProcessor = (_schema, _ctx, json, _params) => {
4608
4617
  json.not = {};
4609
4618
  };
4619
+ var anyProcessor = (_schema, _ctx, _json, _params) => {};
4620
+ var unknownProcessor = (_schema, _ctx, _json, _params) => {};
4610
4621
  var dateProcessor = (_schema, ctx, _json, _params) => {
4611
4622
  if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
4612
4623
  };
@@ -4638,12 +4649,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
4638
4649
  json.enum = vals;
4639
4650
  }
4640
4651
  };
4652
+ var nanProcessor = (_schema, ctx, _json, _params) => {
4653
+ if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
4654
+ };
4655
+ var templateLiteralProcessor = (schema, _ctx, json, _params) => {
4656
+ const _json = json;
4657
+ const pattern = schema._zod.pattern;
4658
+ if (!pattern) throw new Error("Pattern not found in template literal");
4659
+ _json.type = "string";
4660
+ _json.pattern = pattern.source;
4661
+ };
4662
+ var fileProcessor = (schema, _ctx, json, _params) => {
4663
+ const _json = json;
4664
+ const file = {
4665
+ type: "string",
4666
+ format: "binary",
4667
+ contentEncoding: "binary"
4668
+ };
4669
+ const { minimum, maximum, mime } = schema._zod.bag;
4670
+ if (minimum !== void 0) file.minLength = minimum;
4671
+ if (maximum !== void 0) file.maxLength = maximum;
4672
+ if (mime) if (mime.length === 1) {
4673
+ file.contentMediaType = mime[0];
4674
+ Object.assign(_json, file);
4675
+ } else {
4676
+ Object.assign(_json, file);
4677
+ _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
4678
+ }
4679
+ else Object.assign(_json, file);
4680
+ };
4681
+ var successProcessor = (_schema, _ctx, json, _params) => {
4682
+ json.type = "boolean";
4683
+ };
4641
4684
  var customProcessor = (_schema, ctx, _json, _params) => {
4642
4685
  if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
4643
4686
  };
4687
+ var functionProcessor = (_schema, ctx, _json, _params) => {
4688
+ if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
4689
+ };
4644
4690
  var transformProcessor = (_schema, ctx, _json, _params) => {
4645
4691
  if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
4646
4692
  };
4693
+ var mapProcessor = (_schema, ctx, _json, _params) => {
4694
+ if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
4695
+ };
4696
+ var setProcessor = (_schema, ctx, _json, _params) => {
4697
+ if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
4698
+ };
4647
4699
  var arrayProcessor = (schema, ctx, _json, params) => {
4648
4700
  const json = _json;
4649
4701
  const def = schema._zod.def;
@@ -4848,6 +4900,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
4848
4900
  seen.ref = def.innerType;
4849
4901
  json.readOnly = true;
4850
4902
  };
4903
+ var promiseProcessor = (schema, ctx, _json, params) => {
4904
+ const def = schema._zod.def;
4905
+ process$1(def.innerType, ctx, params);
4906
+ const seen = ctx.seen.get(schema);
4907
+ seen.ref = def.innerType;
4908
+ };
4851
4909
  var optionalProcessor = (schema, ctx, _json, params) => {
4852
4910
  const def = schema._zod.def;
4853
4911
  process$1(def.innerType, ctx, params);
@@ -4860,6 +4918,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
4860
4918
  const seen = ctx.seen.get(schema);
4861
4919
  seen.ref = innerType;
4862
4920
  };
4921
+ var allProcessors = {
4922
+ string: stringProcessor,
4923
+ number: numberProcessor,
4924
+ boolean: booleanProcessor,
4925
+ bigint: bigintProcessor,
4926
+ symbol: symbolProcessor,
4927
+ null: nullProcessor,
4928
+ undefined: undefinedProcessor,
4929
+ void: voidProcessor,
4930
+ never: neverProcessor,
4931
+ any: anyProcessor,
4932
+ unknown: unknownProcessor,
4933
+ date: dateProcessor,
4934
+ enum: enumProcessor,
4935
+ literal: literalProcessor,
4936
+ nan: nanProcessor,
4937
+ template_literal: templateLiteralProcessor,
4938
+ file: fileProcessor,
4939
+ success: successProcessor,
4940
+ custom: customProcessor,
4941
+ function: functionProcessor,
4942
+ transform: transformProcessor,
4943
+ map: mapProcessor,
4944
+ set: setProcessor,
4945
+ array: arrayProcessor,
4946
+ object: objectProcessor,
4947
+ union: unionProcessor,
4948
+ intersection: intersectionProcessor,
4949
+ tuple: tupleProcessor,
4950
+ record: recordProcessor,
4951
+ nullable: nullableProcessor,
4952
+ nonoptional: nonoptionalProcessor,
4953
+ default: defaultProcessor,
4954
+ prefault: prefaultProcessor,
4955
+ catch: catchProcessor,
4956
+ pipe: pipeProcessor,
4957
+ readonly: readonlyProcessor,
4958
+ promise: promiseProcessor,
4959
+ optional: optionalProcessor,
4960
+ lazy: lazyProcessor
4961
+ };
4962
+ function toJSONSchema(input, params) {
4963
+ if ("_idmap" in input) {
4964
+ const registry = input;
4965
+ const ctx = initializeContext({
4966
+ ...params,
4967
+ processors: allProcessors
4968
+ });
4969
+ const defs = {};
4970
+ for (const entry of registry._idmap.entries()) {
4971
+ const [_, schema] = entry;
4972
+ process$1(schema, ctx);
4973
+ }
4974
+ const schemas = {};
4975
+ ctx.external = {
4976
+ registry,
4977
+ uri: params?.uri,
4978
+ defs
4979
+ };
4980
+ for (const entry of registry._idmap.entries()) {
4981
+ const [key, schema] = entry;
4982
+ extractDefs(ctx, schema);
4983
+ schemas[key] = finalize(ctx, schema);
4984
+ }
4985
+ if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
4986
+ return { schemas };
4987
+ }
4988
+ const ctx = initializeContext({
4989
+ ...params,
4990
+ processors: allProcessors
4991
+ });
4992
+ process$1(input, ctx);
4993
+ extractDefs(ctx, input);
4994
+ return finalize(ctx, input);
4995
+ }
4863
4996
  //#endregion
4864
4997
  //#region ../../node_modules/zod/v4/classic/iso.js
4865
4998
  var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
@@ -5884,7 +6017,7 @@ var ZodIssueCode = {
5884
6017
  var ZodFirstPartyTypeKind;
5885
6018
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5886
6019
  //#endregion
5887
- //#region ../types/dist/sleep-DV4qxEyD.mjs
6020
+ //#region ../types/dist/sleep-D1jjWAr8.mjs
5888
6021
  /**
5889
6022
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5890
6023
  * window to float samples (D455).
@@ -6032,9 +6165,16 @@ function isSecretMarker(value) {
6032
6165
  const ref = value["$secret"];
6033
6166
  return typeof ref === "string" && ref.length > 0;
6034
6167
  }
6168
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6169
+ function isSecretMarkerLike(value) {
6170
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6171
+ }
6035
6172
  function isSecretClear(value) {
6036
6173
  return isRecord$7(value) && value["$secretClear"] === true;
6037
6174
  }
6175
+ function isSecretFieldView(value) {
6176
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6177
+ }
6038
6178
  /**
6039
6179
  * The UI view of a stored secret field value: a marker becomes "set", inline
6040
6180
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6416,6 +6556,53 @@ function emitReadiness(bus, params) {
6416
6556
  ts
6417
6557
  }));
6418
6558
  }
6559
+ /** Why a catalogue entry is not a runnable body of its declared kind. */
6560
+ var AddonMigrationDefinitionError = class extends Error {
6561
+ id;
6562
+ constructor(id, message) {
6563
+ super(`${id}: ${message}`);
6564
+ this.id = id;
6565
+ this.name = "AddonMigrationDefinitionError";
6566
+ }
6567
+ };
6568
+ /**
6569
+ * Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
6570
+ * an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
6571
+ * body). A throwing body never escapes `execute`: it is reported as a failed result.
6572
+ */
6573
+ function defineAddonMigrations(catalogue, ports) {
6574
+ return catalogue.map((entry) => {
6575
+ const { meta } = entry;
6576
+ if ("plan" in entry) {
6577
+ if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
6578
+ return {
6579
+ meta,
6580
+ execute: () => guarded(async () => ({
6581
+ kind: "plan",
6582
+ plan: await entry.plan(ports)
6583
+ }))
6584
+ };
6585
+ }
6586
+ if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
6587
+ return {
6588
+ meta,
6589
+ execute: () => guarded(async () => ({
6590
+ kind: "procedure",
6591
+ result: await entry.procedure(ports)
6592
+ }))
6593
+ };
6594
+ });
6595
+ }
6596
+ async function guarded(work) {
6597
+ try {
6598
+ return await work();
6599
+ } catch (err) {
6600
+ return {
6601
+ kind: "failed",
6602
+ error: errMsg(err)
6603
+ };
6604
+ }
6605
+ }
6419
6606
  var NOT_RUN = {
6420
6607
  declared: false,
6421
6608
  snapshot: null,
@@ -6430,7 +6617,7 @@ var NOT_RUN = {
6430
6617
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6431
6618
  * because the hub (and this addon's boot) must not hang on a migration.
6432
6619
  */
6433
- async function runAddonMigrations(host, migrations, logger) {
6620
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6434
6621
  const first = migrations[0];
6435
6622
  if (first === void 0) return NOT_RUN;
6436
6623
  const owner = first.meta.owner;
@@ -6459,7 +6646,9 @@ async function runAddonMigrations(host, migrations, logger) {
6459
6646
  else if (status === "blocked") blocked.push(id);
6460
6647
  else skipped.push(id);
6461
6648
  };
6649
+ const selected = options.only === void 0 ? null : new Set(options.only);
6462
6650
  for (const step of declaration.steps) {
6651
+ if (selected !== null && !selected.has(step.id)) continue;
6463
6652
  const bound = byId.get(step.id);
6464
6653
  if (bound === void 0 || step.action === "skip") {
6465
6654
  skipped.push(step.id);
@@ -6637,11 +6826,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6637
6826
  }
6638
6827
  return out;
6639
6828
  }
6829
+ /**
6830
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6831
+ *
6832
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6833
+ * scope (global, one node, one device, one integration, one row of a business
6834
+ * collection) and the field path inside that scope — so saving the same field
6835
+ * twice updates the same ref, and the ref itself says which settings blob
6836
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6837
+ * copied onto another ref does not decrypt.
6838
+ *
6839
+ * Array items are addressed by ITEM ID, never by index
6840
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6841
+ *
6842
+ * Pure and browser-safe: no node builtins, no I/O.
6843
+ */
6844
+ /** The URI scheme of every keychain ref. */
6845
+ var SECRET_REF_SCHEME = "secret://";
6640
6846
  var GLOBAL_SCOPE = { kind: "global" };
6641
6847
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6642
6848
  function globalSecretScope() {
6643
6849
  return GLOBAL_SCOPE;
6644
6850
  }
6851
+ /**
6852
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6853
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6854
+ * addon running forked.
6855
+ */
6856
+ function normalizeSecretOwner(addonId) {
6857
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6858
+ }
6645
6859
  function encodeSegment(value) {
6646
6860
  return encodeURIComponent(value);
6647
6861
  }
@@ -6652,6 +6866,16 @@ function decodeSegment(value) {
6652
6866
  return null;
6653
6867
  }
6654
6868
  }
6869
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6870
+ function formatSecretScope(scope) {
6871
+ switch (scope.kind) {
6872
+ case "global": return "global";
6873
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6874
+ case "device": return `device/${scope.deviceId}`;
6875
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6876
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6877
+ }
6878
+ }
6655
6879
  /**
6656
6880
  * The field path of a value inside an array item, addressed by the item's id:
6657
6881
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6660,6 +6884,13 @@ function decodeSegment(value) {
6660
6884
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6661
6885
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6662
6886
  }
6887
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6888
+ function buildSecretRef(parts) {
6889
+ const owner = normalizeSecretOwner(parts.owner);
6890
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6891
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6892
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6893
+ }
6663
6894
  function parseScope(segments) {
6664
6895
  switch (segments[0]) {
6665
6896
  case "global": return {
@@ -7577,14 +7808,18 @@ var BaseAddon = class {
7577
7808
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7578
7809
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7579
7810
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7811
+ *
7812
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7813
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7814
+ * omitted applied ids "newer").
7580
7815
  */
7581
- async runMigrations(migrations) {
7816
+ async runMigrations(migrations, options) {
7582
7817
  const api = this.ctx.api.migrations;
7583
7818
  const report = await runAddonMigrations({
7584
7819
  declareOwner: (input) => api.declareOwner.mutate(input),
7585
7820
  startMigration: (input) => api.startMigration.mutate(input),
7586
7821
  finishMigration: (input) => api.finishMigration.mutate(input)
7587
- }, migrations, this.ctx.logger);
7822
+ }, migrations, this.ctx.logger, options);
7588
7823
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7589
7824
  return report;
7590
7825
  }
@@ -8568,6 +8803,45 @@ function normalizeAddonInitResult(result) {
8568
8803
  if (Array.isArray(result)) return { providers: result };
8569
8804
  return result;
8570
8805
  }
8806
+ var PREFIX = "records/";
8807
+ /** The base of every records failure. `detail` is the human part of the message. */
8808
+ var RecordsError = class extends Error {
8809
+ code;
8810
+ detail;
8811
+ constructor(code, detail) {
8812
+ super(`${PREFIX}${code}: ${detail}`);
8813
+ this.name = "RecordsError";
8814
+ this.code = code;
8815
+ this.detail = detail;
8816
+ }
8817
+ };
8818
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
8819
+ var RevConflictError = class extends RecordsError {
8820
+ constructor(detail) {
8821
+ super("rev-conflict", detail);
8822
+ this.name = "RevConflictError";
8823
+ }
8824
+ };
8825
+ /** The keychain could not store or serve a value: explicit, never an empty string. */
8826
+ var RecordSecretUnavailableError = class extends RecordsError {
8827
+ constructor(detail) {
8828
+ super("secret-unavailable", detail);
8829
+ this.name = "RecordSecretUnavailableError";
8830
+ }
8831
+ };
8832
+ var InvalidRecordError = class extends RecordsError {
8833
+ constructor(detail) {
8834
+ super("invalid-record", detail);
8835
+ this.name = "InvalidRecordError";
8836
+ }
8837
+ };
8838
+ /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
8839
+ var DeclarationRefusedError = class extends RecordsError {
8840
+ constructor(detail) {
8841
+ super("declaration-refused", detail);
8842
+ this.name = "DeclarationRefusedError";
8843
+ }
8844
+ };
8571
8845
  /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8572
8846
  /** The files a kind may live in. `history` is device-history's (D850), never records. */
8573
8847
  var RECORD_DATABASES = ["settings", "events"];
@@ -8583,6 +8857,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8583
8857
  var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8584
8858
  var RECORD_BATCH_MAX = 1e3;
8585
8859
  var RECORD_QUERY_MAX = 1e3;
8860
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8861
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
8862
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8863
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8864
+ /**
8865
+ * Secret fields of a record, addressed the way a Zod schema declares them
8866
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8867
+ * (`auth.accessToken`, `headers[].value`).
8868
+ *
8869
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8870
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8871
+ * key releases exactly that key's secret.
8872
+ *
8873
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8874
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8875
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8876
+ * leaf names its element: `headers[X-Api].value`.
8877
+ *
8878
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8879
+ */
8880
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8881
+ function parseSecretPath(path) {
8882
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8883
+ return path.split(".").map((raw) => {
8884
+ const each = raw.endsWith("[]");
8885
+ const map = raw.endsWith("{}");
8886
+ const key = each || map ? raw.slice(0, -2) : raw;
8887
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8888
+ return {
8889
+ key,
8890
+ each,
8891
+ map
8892
+ };
8893
+ });
8894
+ }
8895
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8896
+ function secretArrayPrefixes(path) {
8897
+ const segments = parseSecretPath(path);
8898
+ const prefixes = [];
8899
+ const walked = [];
8900
+ for (const segment of segments) {
8901
+ walked.push(segment.key);
8902
+ if (segment.each) prefixes.push(walked.join("."));
8903
+ }
8904
+ return prefixes;
8905
+ }
8906
+ function isRecord$4(value) {
8907
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8908
+ }
8909
+ function elementId(element, idKey, declared, prefix) {
8910
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8911
+ 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`);
8912
+ return id;
8913
+ }
8914
+ /**
8915
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8916
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8917
+ * result is a new body, `data` is untouched.
8918
+ */
8919
+ var REMOVE_LEAF = Symbol("remove-leaf");
8920
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8921
+ if (!isRecord$4(node)) return node;
8922
+ const segment = segments[index];
8923
+ if (segment === void 0) return node;
8924
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8925
+ const child = node[segment.key];
8926
+ const keys = [...walkedKeys, segment.key];
8927
+ const last = index === segments.length - 1;
8928
+ if (segment.each) {
8929
+ if (!Array.isArray(child)) return node;
8930
+ const prefix = keys.join(".");
8931
+ const idKey = arrayKeys[prefix];
8932
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8933
+ const mapped = child.map((element) => {
8934
+ const id = elementId(element, idKey, declared, prefix);
8935
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8936
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8937
+ });
8938
+ return {
8939
+ ...node,
8940
+ [segment.key]: mapped
8941
+ };
8942
+ }
8943
+ if (segment.map) {
8944
+ if (!isRecord$4(child)) return node;
8945
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8946
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8947
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8948
+ return [mapKey, visit({
8949
+ declared,
8950
+ concrete,
8951
+ value
8952
+ })];
8953
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8954
+ return {
8955
+ ...node,
8956
+ [segment.key]: Object.fromEntries(kept)
8957
+ };
8958
+ }
8959
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8960
+ if (last) {
8961
+ const replacement = visit({
8962
+ declared,
8963
+ concrete,
8964
+ value: child
8965
+ });
8966
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8967
+ return {
8968
+ ...node,
8969
+ [segment.key]: replacement
8970
+ };
8971
+ }
8972
+ return {
8973
+ ...node,
8974
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8975
+ };
8976
+ }
8977
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8978
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8979
+ let current = data;
8980
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8981
+ return current;
8982
+ }
8983
+ /** Every present secret leaf, read-only. */
8984
+ function collectSecretLeaves(data, paths, arrayKeys) {
8985
+ const found = [];
8986
+ mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
8987
+ found.push(leaf);
8988
+ return leaf.value;
8989
+ });
8990
+ return found;
8991
+ }
8992
+ /**
8993
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8994
+ * record's refs never collide with the refs a legacy collection of the same name
8995
+ * already holds during an adoption.
8996
+ */
8997
+ function recordSecretCollection(kind) {
8998
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8999
+ }
9000
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
9001
+ function recordSecretRef(owner, kind, rowId, concretePath) {
9002
+ return buildSecretRef({
9003
+ owner,
9004
+ scope: {
9005
+ kind: "row",
9006
+ collection: recordSecretCollection(kind),
9007
+ rowId
9008
+ },
9009
+ fieldPath: concretePath
9010
+ });
9011
+ }
9012
+ /**
9013
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
9014
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
9015
+ * token. The form hands it to the secret field, which reveals the value by ref
9016
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
9017
+ * stored secret (the records door reads a view as KEEP).
9018
+ *
9019
+ * ONE implementation for the entries service and for every addon that serves
9020
+ * its own instances (`RecordIntegrationInstances`).
9021
+ */
9022
+ function isRecord$3(value) {
9023
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9024
+ }
9025
+ function secretViewsOf(owner, kind, id, decl, data) {
9026
+ if (decl.secretPaths.length === 0) return data;
9027
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
9028
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
9029
+ return {
9030
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
9031
+ v: 1,
9032
+ set: true
9033
+ };
9034
+ });
9035
+ return isRecord$3(shown) ? shown : data;
9036
+ }
8586
9037
  /**
8587
9038
  * The share-view scope kinds a share-link token can carry. The hub mints
8588
9039
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8655,7 +9106,8 @@ function isZodSecret(schema) {
8655
9106
  }
8656
9107
  /**
8657
9108
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8658
- * element of this array" (`connection.password`, `rows[].token`). A declared
9109
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9110
+ * map" (`applianceSecrets{}`). A declared
8659
9111
  * object or array is reported as one path and not descended into — everything
8660
9112
  * under it is secret.
8661
9113
  */
@@ -8666,6 +9118,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8666
9118
  const def = defOf(layer);
8667
9119
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8668
9120
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9121
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
8669
9122
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8670
9123
  else if (def.type === "intersection") {
8671
9124
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -8740,6 +9193,126 @@ var RecordKindDeclarationSchema = object({
8740
9193
  }).strict();
8741
9194
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8742
9195
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9196
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9197
+ function assertValidDeclaration(name, decl) {
9198
+ const refuse = (why) => {
9199
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9200
+ };
9201
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9202
+ const seen = /* @__PURE__ */ new Set();
9203
+ for (const index of decl.indexes) {
9204
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9205
+ seen.add(index.name);
9206
+ }
9207
+ const { retention } = decl;
9208
+ const hasSecrets = decl.secretPaths.length > 0;
9209
+ if ("ttlMs" in retention) {
9210
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9211
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9212
+ }
9213
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9214
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9215
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9216
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9217
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9218
+ 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)`);
9219
+ }
9220
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9221
+ 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`);
9222
+ }
9223
+ function bodyFieldsOf(schema) {
9224
+ return { ...toJSONSchema(schema, {
9225
+ unrepresentable: "any",
9226
+ io: "input"
9227
+ }) };
9228
+ }
9229
+ function isRecordLike(value) {
9230
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9231
+ }
9232
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9233
+ function hasNameField(json, field) {
9234
+ const branches = json["oneOf"] ?? json["anyOf"];
9235
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9236
+ const properties = json["properties"];
9237
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9238
+ }
9239
+ /**
9240
+ * Declare the kinds of a package. The keys are the kind names. The result is
9241
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9242
+ * types its handle from.
9243
+ */
9244
+ function defineRecordKinds(specs) {
9245
+ const out = {};
9246
+ for (const [name, spec] of Object.entries(specs)) {
9247
+ const secretPaths = collectZodSecretPaths(spec.schema);
9248
+ const nameField = spec.entry?.nameField ?? "name";
9249
+ const fields = bodyFieldsOf(spec.schema);
9250
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9251
+ const parsed = RecordKindDeclarationSchema.parse({
9252
+ database: spec.database,
9253
+ class: spec.class,
9254
+ writeTrigger: spec.writeTrigger,
9255
+ device: spec.device,
9256
+ at: spec.at,
9257
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9258
+ retention: spec.retention,
9259
+ deviceRemoval: spec.deviceRemoval,
9260
+ maxDataBytes: spec.maxDataBytes,
9261
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9262
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9263
+ transactFamily: spec.transactFamily,
9264
+ entry: spec.entry === void 0 ? void 0 : {
9265
+ title: spec.entry.title,
9266
+ nameField,
9267
+ fields
9268
+ }
9269
+ });
9270
+ assertValidDeclaration(name, parsed);
9271
+ out[name] = {
9272
+ name,
9273
+ schema: spec.schema,
9274
+ declaration: parsed
9275
+ };
9276
+ }
9277
+ return out;
9278
+ }
9279
+ /**
9280
+ * Entry kinds (D881): every list of user-created entries (notification
9281
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9282
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9283
+ * through the `record-entries` capability with password fields derived from
9284
+ * the schema (`.meta({ secret: true })`).
9285
+ */
9286
+ /** The body of a plain entry: a name and the fields the kind adds. */
9287
+ function entryBody(shape) {
9288
+ return object({
9289
+ name: string().min(1),
9290
+ ...shape
9291
+ });
9292
+ }
9293
+ /**
9294
+ * Declare one entry kind. An entry is configuration: never trimmed by the
9295
+ * engine, never aged, belonging to no device.
9296
+ */
9297
+ function defineEntryKind(name, spec) {
9298
+ const defined = defineRecordKinds({ [name]: {
9299
+ schema: spec.schema,
9300
+ class: "config",
9301
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9302
+ retention: { none: "an entry goes only when the operator deletes it" },
9303
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9304
+ ...spec.database === void 0 ? {} : { database: spec.database },
9305
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9306
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9307
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9308
+ entry: {
9309
+ title: spec.title,
9310
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9311
+ }
9312
+ } })[name];
9313
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9314
+ return defined;
9315
+ }
8743
9316
  object({
8744
9317
  field: string().min(1),
8745
9318
  on: literal("advance")
@@ -11419,7 +11992,7 @@ object({
11419
11992
  params: record(string(), union([string(), number()])).optional()
11420
11993
  });
11421
11994
  //#endregion
11422
- //#region ../types/dist/composition-BodkvOV0.mjs
11995
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11423
11996
  /**
11424
11997
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11425
11998
  *
@@ -12182,38 +12755,6 @@ function validateExpressionSource(src) {
12182
12755
  return null;
12183
12756
  }
12184
12757
  /**
12185
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12186
- *
12187
- * Standby of the control board is under 5 W; a motor or a heater is far
12188
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12189
- * a cycle, so running starts only after 30 s above the line.
12190
- *
12191
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12192
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12193
- * report "finished" in the middle of the program, so the stop dwell is
12194
- * 10 minutes on every type. The number is stored per type so one machine
12195
- * can move later without moving the others.
12196
- */
12197
- var APPLIANCE_TEMPLATE_ID = "appliance";
12198
- var APPLIANCE_KINDS = [
12199
- "washer",
12200
- "dryer",
12201
- "dishwasher"
12202
- ];
12203
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12204
- /**
12205
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12206
- *
12207
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12208
- * (D967), so the heater switches exactly as the plug does. Running is the
12209
- * plug being on; when a power reading is also named, running is the draw
12210
- * staying above the line instead (a plug left on with the stove's own
12211
- * thermostat open is not heating). 50 W is far above any standby and below
12212
- * the smallest radiator.
12213
- */
12214
- var HEATER_TEMPLATE_ID = "heater";
12215
- DeviceType.Heater;
12216
- /**
12217
12758
  * Statistics and integrals over a history read (D944).
12218
12759
  *
12219
12760
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12357,34 +12898,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12357
12898
  /** Actuator groups only. Readings leave this unset. */
12358
12899
  active: ActiveCriterionSchema.optional()
12359
12900
  }),
12360
- object({
12361
- kind: literal("appliance"),
12362
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12363
- appliance: _enum(APPLIANCE_KINDS),
12364
- thresholdWatts: number().positive(),
12365
- startDwellMs: number().int().nonnegative(),
12366
- stopDwellMs: number().int().nonnegative(),
12367
- member: HelperMemberSchema,
12368
- /**
12369
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12370
- * composition carries a `power-source` feature, so the hub can cut and restore
12371
- * that plug (power-source link of a NEW-target composition).
12372
- */
12373
- powerSwitch: HelperMemberSchema.optional(),
12374
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12375
- offSeconds: number().optional()
12376
- }),
12377
- object({
12378
- kind: literal("heater"),
12379
- templateId: literal(HEATER_TEMPLATE_ID),
12380
- /** The plug, read and commanded at `switch.on`. */
12381
- plug: HelperMemberSchema,
12382
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12383
- meter: HelperMemberSchema.optional(),
12384
- thresholdWatts: number().positive(),
12385
- startDwellMs: number().int().nonnegative(),
12386
- stopDwellMs: number().int().nonnegative()
12387
- }),
12388
12901
  object({
12389
12902
  kind: literal("statistic"),
12390
12903
  stat: _enum(STATISTIC_STATS),
@@ -12608,7 +13121,7 @@ var CompositionSchema = object({
12608
13121
  kind: literal("existing"),
12609
13122
  device: CompositionSourceRefSchema
12610
13123
  })]),
12611
- features: array(CompositionFeatureSchema).min(1).max(16),
13124
+ features: array(CompositionFeatureSchema).min(0).max(16),
12612
13125
  /**
12613
13126
  * Present when the block is a helper template. Absent on a free
12614
13127
  * composition (the room example, device 4603). The editor reads this,
@@ -12826,6 +13339,205 @@ var IntegrationInstanceRefSchema = object({
12826
13339
  ]),
12827
13340
  candidateStableId: string().optional()
12828
13341
  });
13342
+ /**
13343
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13344
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13345
+ *
13346
+ * Every instance is one record of the addon's own kind, keyed by the
13347
+ * `integrations` registry id. The records door does the secret work: a field the
13348
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13349
+ * deleting the row deletes the secret, and one row's value is never another's.
13350
+ *
13351
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13352
+ * token or the view the form was given KEEP the stored secret;
13353
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13354
+ */
13355
+ /** The most instances one addon lists (an integrations page, not a table). */
13356
+ var INSTANCES_LIST_MAX = 1e3;
13357
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13358
+ function topLevelSecretKeys(paths) {
13359
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13360
+ }
13361
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13362
+ function secretMapKeys(paths) {
13363
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13364
+ }
13365
+ /**
13366
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13367
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13368
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13369
+ */
13370
+ function secretMapPatch(value) {
13371
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13372
+ const out = {};
13373
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13374
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13375
+ return Object.keys(out).length > 0 ? out : null;
13376
+ }
13377
+ /** Does the kind's body schema carry an `enabled` field? */
13378
+ function declaresEnabled(kind) {
13379
+ const json = toJSONSchema(kind.schema, {
13380
+ unrepresentable: "any",
13381
+ io: "input"
13382
+ });
13383
+ const properties = Reflect.get(json, "properties");
13384
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13385
+ }
13386
+ var RecordIntegrationInstances = class {
13387
+ records;
13388
+ kind;
13389
+ hooks;
13390
+ handle;
13391
+ secretKeys;
13392
+ secretMaps;
13393
+ nameField;
13394
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13395
+ registryOwnsEnabled;
13396
+ constructor(records, kind, hooks) {
13397
+ this.records = records;
13398
+ this.kind = kind;
13399
+ this.hooks = hooks;
13400
+ this.handle = records.kind(kind);
13401
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13402
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13403
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13404
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13405
+ 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"`);
13406
+ }
13407
+ /**
13408
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13409
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13410
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13411
+ */
13412
+ async readResolved(id) {
13413
+ try {
13414
+ const record = await this.handle.getResolved(id);
13415
+ return record === null ? null : {
13416
+ id,
13417
+ body: record.data,
13418
+ enabled: record.data.enabled !== false,
13419
+ secrets: "resolved"
13420
+ };
13421
+ } catch (err) {
13422
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13423
+ const stored = await this.handle.get(id);
13424
+ if (stored === null) return null;
13425
+ return {
13426
+ id,
13427
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13428
+ enabled: stored.data.enabled !== false,
13429
+ secrets: "unavailable"
13430
+ };
13431
+ }
13432
+ }
13433
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13434
+ async getResolved(id) {
13435
+ return (await this.readResolved(id))?.body ?? null;
13436
+ }
13437
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13438
+ async listResolved() {
13439
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13440
+ const out = [];
13441
+ for (const record of page.records) {
13442
+ const read = await this.readResolved(record.id);
13443
+ if (read !== null) out.push(read);
13444
+ }
13445
+ return out;
13446
+ }
13447
+ viewOf(record) {
13448
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13449
+ const rawName = record.data[this.nameField];
13450
+ const kind = this.hooks.kindOf?.(record.data);
13451
+ const health = this.hooks.health?.(record.data, record.id);
13452
+ return {
13453
+ id: record.id,
13454
+ name: typeof rawName === "string" ? rawName : record.id,
13455
+ configured: this.hooks.configured(record.data),
13456
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13457
+ formValues: shown,
13458
+ ...kind !== void 0 ? { kind } : {},
13459
+ ...health?.status !== void 0 ? { status: health.status } : {},
13460
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13461
+ };
13462
+ }
13463
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13464
+ ...this.hooks.description,
13465
+ mintsId: true
13466
+ };
13467
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13468
+ listInstances = async () => {
13469
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13470
+ };
13471
+ getInstance = async ({ id }) => {
13472
+ const record = await this.handle.get(id);
13473
+ return record === null ? null : this.viewOf(record);
13474
+ };
13475
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13476
+ createInstance = async ({ id, name, kind }) => {
13477
+ const minted = id ?? this.hooks.mintId?.({
13478
+ name,
13479
+ ...kind !== void 0 ? { kind } : {}
13480
+ });
13481
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13482
+ await this.handle.put({
13483
+ id: minted,
13484
+ data: this.hooks.initialBody(name, kind)
13485
+ }, { ifRev: 0 });
13486
+ await this.hooks.onChanged?.(minted, "created");
13487
+ return { id: minted };
13488
+ };
13489
+ updateInstance = async ({ id, name, values }) => {
13490
+ const patch = {};
13491
+ for (const [key, value] of Object.entries(values)) {
13492
+ if (value === void 0) continue;
13493
+ if (this.secretMaps.has(key)) {
13494
+ const entries = secretMapPatch(value);
13495
+ if (entries !== null) patch[key] = entries;
13496
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13497
+ else if (isSecretClear(value)) patch[key] = null;
13498
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13499
+ }
13500
+ if (name !== void 0) patch[this.nameField] = name;
13501
+ await this.handle.patch(id, patch);
13502
+ await this.hooks.onChanged?.(id, "updated");
13503
+ return { ok: true };
13504
+ };
13505
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13506
+ setInstanceEnabled = async ({ id, enabled }) => {
13507
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13508
+ await this.handle.patch(id, { enabled });
13509
+ await this.hooks.onChanged?.(id, "enabled");
13510
+ return { ok: true };
13511
+ };
13512
+ /** The row and its keychain refs go together (the records door). */
13513
+ deleteInstance = async ({ id }) => {
13514
+ await this.handle.delete(id);
13515
+ await this.hooks.onChanged?.(id, "deleted");
13516
+ return { ok: true };
13517
+ };
13518
+ testInstance = async (input) => {
13519
+ if (this.hooks.test === void 0) return {
13520
+ success: false,
13521
+ status: "unsupported",
13522
+ error: "This integration declares no connection test"
13523
+ };
13524
+ return this.hooks.test(input);
13525
+ };
13526
+ /** The flat provider object to hand to `registerProvider`. */
13527
+ toProvider() {
13528
+ return {
13529
+ describe: this.describe,
13530
+ instanceFormSchema: this.instanceFormSchema,
13531
+ listInstances: this.listInstances,
13532
+ getInstance: this.getInstance,
13533
+ createInstance: this.createInstance,
13534
+ updateInstance: this.updateInstance,
13535
+ setInstanceEnabled: this.setInstanceEnabled,
13536
+ deleteInstance: this.deleteInstance,
13537
+ testInstance: this.testInstance
13538
+ };
13539
+ }
13540
+ };
12829
13541
  var EncodeProfileSchema = object({
12830
13542
  video: object({
12831
13543
  codec: _enum([
@@ -13861,7 +14573,13 @@ var MigrationLedgerEntrySchema = object({
13861
14573
  * joined by the engine from the catalogues it knows this boot. Absent when no
13862
14574
  * catalogue lists the id (a newer database).
13863
14575
  */
13864
- meta: MigrationMetaSchema.optional()
14576
+ meta: MigrationMetaSchema.optional(),
14577
+ /**
14578
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14579
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14580
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14581
+ */
14582
+ ownerNotLoaded: boolean().optional()
13865
14583
  });
13866
14584
  /** A catalogued migration that is not applied, with where it stands. */
13867
14585
  var MigrationPendingEntrySchema = object({
@@ -13895,6 +14613,221 @@ var MigrationOwnerStepSchema = object({
13895
14613
  state: union([literal("pending"), MigrationStatusSchema])
13896
14614
  });
13897
14615
  /**
14616
+ * Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
14617
+ * kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
14618
+ * the integration id, the devices stamped with it and the automations that name it do not change.
14619
+ *
14620
+ * Order, so a crash or a locked keychain is safe at every point:
14621
+ *
14622
+ * 1. Phase A, read only: every row of every integration of the addon is read with its secrets
14623
+ * resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
14624
+ * anything is written; `block-owner` then holds the addon.
14625
+ * 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
14626
+ * `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
14627
+ * and every secret leaf must equal what the rows held, else the record is deleted and the pass
14628
+ * throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
14629
+ * a warning names the path. Only then are the rows (and their keychain values) cleared.
14630
+ *
14631
+ * Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
14632
+ * a no-op. A default integration with no rows still gets its record (an empty body, D985).
14633
+ * Never logs or throws a secret value; messages name integration ids and secret PATHS only.
14634
+ */
14635
+ function secretLeavesOf(body, spec) {
14636
+ const out = /* @__PURE__ */ new Map();
14637
+ 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);
14638
+ return out;
14639
+ }
14640
+ function why(err) {
14641
+ return err instanceof Error ? err.message : String(err);
14642
+ }
14643
+ async function moveIntegrationRowsToRecords(ports, spec) {
14644
+ const integrations = await ports.listIntegrations(spec.addonId);
14645
+ if (integrations.length === 0) return {
14646
+ outcome: "already-done",
14647
+ summary: { integrations: 0 }
14648
+ };
14649
+ const rowsOf = /* @__PURE__ */ new Map();
14650
+ for (const integration of integrations) try {
14651
+ rowsOf.set(integration.id, await ports.readRows(integration.id));
14652
+ } catch (err) {
14653
+ throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
14654
+ }
14655
+ let created = 0;
14656
+ let cleared = 0;
14657
+ let kept = 0;
14658
+ for (const integration of integrations) {
14659
+ const values = rowsOf.get(integration.id) ?? {};
14660
+ const hasRows = Object.keys(values).length > 0;
14661
+ const body = spec.bodyFrom(values, ports, integration);
14662
+ const put = await ports.putRecord(integration.id, body);
14663
+ if (put === "created") created += 1;
14664
+ if (!hasRows) continue;
14665
+ const wanted = secretLeavesOf(body, spec);
14666
+ const back = await ports.readRecord(integration.id);
14667
+ if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
14668
+ const held = secretLeavesOf(back, spec);
14669
+ if (put === "created") {
14670
+ const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
14671
+ if (wrong.length > 0) {
14672
+ await ports.deleteRecord(integration.id);
14673
+ 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`);
14674
+ }
14675
+ } else {
14676
+ const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
14677
+ if (missing.length > 0) {
14678
+ ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
14679
+ kept += 1;
14680
+ continue;
14681
+ }
14682
+ }
14683
+ await ports.clearRows(integration.id);
14684
+ cleared += 1;
14685
+ }
14686
+ return created === 0 && cleared === 0 ? {
14687
+ outcome: "already-done",
14688
+ summary: {
14689
+ integrations: integrations.length,
14690
+ kept
14691
+ }
14692
+ } : {
14693
+ outcome: "done",
14694
+ summary: {
14695
+ integrations: integrations.length,
14696
+ created,
14697
+ cleared,
14698
+ kept
14699
+ }
14700
+ };
14701
+ }
14702
+ /**
14703
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
14704
+ * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
14705
+ * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
14706
+ * the new record through the addon's own records handle. The hub deletes the rows and their
14707
+ * keychain values; the addon never touches the registry.
14708
+ */
14709
+ function integrationRowsPortsOverApi(deps) {
14710
+ return {
14711
+ warn: (message) => deps.log.warn(message),
14712
+ listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
14713
+ id: i.id,
14714
+ name: i.name
14715
+ })),
14716
+ readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
14717
+ putRecord: async (id, body) => {
14718
+ if (await deps.records.get(id) !== null) return "exists";
14719
+ try {
14720
+ await deps.records.put({
14721
+ id,
14722
+ data: body
14723
+ }, { ifRev: 0 });
14724
+ } catch (err) {
14725
+ if (err instanceof RevConflictError) return "exists";
14726
+ throw err;
14727
+ }
14728
+ return "created";
14729
+ },
14730
+ readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
14731
+ deleteRecord: async (id) => {
14732
+ await deps.records.delete(id);
14733
+ },
14734
+ clearRows: async (integrationId) => {
14735
+ await deps.api.clearSettings({ id: integrationId });
14736
+ }
14737
+ };
14738
+ }
14739
+ /**
14740
+ * The rows-to-record migration of a device addon whose integrations hold REAL settings
14741
+ * (planning#416, S-B): every integration of the addon becomes a record of the addon's own kind
14742
+ * under its registry id, the record body built from the legacy `integration_settings` rows, and
14743
+ * the rows and their keychain values are cleared once the record is read back
14744
+ * (`moveIntegrationRowsToRecords`). The addon declares ONE entry kind; this file turns that kind
14745
+ * into the body, the migration and its ports, so an addon states nothing but the kind.
14746
+ *
14747
+ * It runs in the addon process through the hub API, BEFORE the addon registers its
14748
+ * `integration-instances` provider, and nothing reads the old rows afterwards.
14749
+ */
14750
+ /**
14751
+ * The record body of one integration from its resolved legacy rows. A key the kind has no field
14752
+ * for, or whose value the field refuses, is dropped and named in a warning (keys only, never a
14753
+ * value); a blank value is no value. A secret the keychain could not serve (a marker, not a
14754
+ * string) throws: moving it would lose it, and the rows are still there.
14755
+ */
14756
+ function integrationBodyFromRows(kind, values, log, integration) {
14757
+ const fields = new Map(Object.entries(kind.schema.shape));
14758
+ const body = { name: integration.name };
14759
+ const dropped = [];
14760
+ for (const [key, value] of Object.entries(values)) {
14761
+ if (isSecretMarkerLike(value)) throw new Error(`integration '${integration.id}': ${key} could not be read from the keychain - nothing moved`);
14762
+ if (value === void 0 || value === null || value === "") continue;
14763
+ const field = key === "name" ? void 0 : fields.get(key);
14764
+ if (field === void 0 || !field.safeParse(value).success) {
14765
+ dropped.push(key);
14766
+ continue;
14767
+ }
14768
+ body[key] = value;
14769
+ }
14770
+ if (dropped.length > 0) log.warn(`integration '${integration.id}': legacy settings the ${kind.name} record has no valid field for are dropped: ${dropped.join(", ")}`);
14771
+ return kind.schema.parse(body);
14772
+ }
14773
+ function integrationRowsToRecordId(addonId, date) {
14774
+ return `${addonId}/${date}-integration-rows-to-record`;
14775
+ }
14776
+ function integrationRowsMigration(spec) {
14777
+ return {
14778
+ meta: {
14779
+ id: integrationRowsToRecordId(spec.addonId, spec.date),
14780
+ owner: spec.addonId,
14781
+ level: "addon-state",
14782
+ seq: 1,
14783
+ dependsOn: [],
14784
+ store: "settings",
14785
+ scope: "cluster",
14786
+ onFailure: "block-owner",
14787
+ bodyKind: "procedure",
14788
+ description: "Move every integration of the addon into a typed instance record keyed by its registry id, with its connection settings and secrets, then clear the legacy integration settings rows",
14789
+ fixtureMark: spec.fixtureMark
14790
+ },
14791
+ procedure: (ports) => moveIntegrationRowsToRecords(ports, {
14792
+ addonId: spec.addonId,
14793
+ secretPaths: spec.kind.declaration.secretPaths,
14794
+ secretArrayKeys: spec.kind.declaration.secretArrayKeys,
14795
+ bodyFrom: (values, log, integration) => integrationBodyFromRows(spec.kind, values, log, integration)
14796
+ })
14797
+ };
14798
+ }
14799
+ /** The hub and records sides of {@link integrationRowsMigration}. */
14800
+ function integrationRowsPortsOver(deps) {
14801
+ return integrationRowsPortsOverApi({
14802
+ api: deps.api,
14803
+ records: deps.records.kind({
14804
+ name: deps.kind.name,
14805
+ declaration: deps.kind.declaration,
14806
+ schema: record(string(), unknown())
14807
+ }),
14808
+ log: { warn: deps.warn }
14809
+ });
14810
+ }
14811
+ function defaultIntegrationApiOver(api) {
14812
+ return {
14813
+ list: () => api.integrations.list.query(),
14814
+ getSettings: (input) => api.integrations.getSettings.query(input),
14815
+ clearSettings: (input) => api.integrations.clearSettings.mutate(input),
14816
+ listDevices: (addonId) => api.deviceManager.listAll.query({
14817
+ addonId,
14818
+ projection: "slim"
14819
+ }),
14820
+ setDeviceIntegrationId: async (deviceId, integrationId) => {
14821
+ await api.deviceManager.setIntegrationId.mutate({
14822
+ deviceId,
14823
+ integrationId
14824
+ });
14825
+ },
14826
+ readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
14827
+ readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
14828
+ };
14829
+ }
14830
+ /**
13898
14831
  * The trainer contract — what the hub hands the cloud trainer and what it
13899
14832
  * gets back (cloud training, D805+).
13900
14833
  *
@@ -49379,117 +50312,6 @@ function actuator(input) {
49379
50312
  };
49380
50313
  }
49381
50314
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
49382
- var PowerSourceRefusalReasonSchema = _enum([
49383
- "no-power-source",
49384
- "switch-missing",
49385
- "switch-offline",
49386
- "switch-unknown",
49387
- "restart-in-progress",
49388
- "switch-call-failed"
49389
- ]);
49390
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
49391
- accepted: literal(false),
49392
- reason: PowerSourceRefusalReasonSchema,
49393
- /** One line for the operator, naming the switch. */
49394
- message: string()
49395
- })]);
49396
- var PowerSourceInfoSchema = object({
49397
- /** False when the device has no power source linked; the rest is then empty. */
49398
- linked: boolean(),
49399
- switchDeviceId: number().nullable(),
49400
- switchName: string().nullable(),
49401
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
49402
- switchOn: boolean().nullable(),
49403
- switchOnline: boolean().nullable(),
49404
- restartInProgress: boolean(),
49405
- offSeconds: number().int()
49406
- });
49407
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
49408
- var PowerSourceStateSchema = object({
49409
- /** Null while the switch has not reported: unknown is never `false` (D393). */
49410
- switchOn: boolean().nullable(),
49411
- offSeconds: number().int(),
49412
- lastFetchedAt: number()
49413
- });
49414
- var POWER_SOURCE_DEVICE_TYPES = [
49415
- DeviceType.Gateway,
49416
- DeviceType.Hub,
49417
- DeviceType.Camera,
49418
- DeviceType.Sensor,
49419
- DeviceType.MediaPlayer,
49420
- DeviceType.Vacuum,
49421
- DeviceType.Generic,
49422
- DeviceType.Washer,
49423
- DeviceType.Dryer,
49424
- DeviceType.Dishwasher,
49425
- DeviceType.PetFountain,
49426
- DeviceType.Fan,
49427
- DeviceType.Humidifier,
49428
- DeviceType.Climate,
49429
- DeviceType.WaterHeater
49430
- ];
49431
- var powerSourceCustomize = {
49432
- version: 1,
49433
- targets: [{
49434
- id: "switch",
49435
- cardinality: "scalar",
49436
- vocabulary: {
49437
- cap: "power-source",
49438
- stateField: "switchOn"
49439
- },
49440
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49441
- apply: "add",
49442
- path: "switchOn",
49443
- sources: [{
49444
- kind: "switch",
49445
- transforms: [{ kind: "identity" }]
49446
- }],
49447
- companions: [{
49448
- kind: "constant",
49449
- path: "offSeconds",
49450
- value: 10,
49451
- when: "add"
49452
- }, {
49453
- kind: "clock",
49454
- path: "lastFetchedAt",
49455
- when: "always"
49456
- }],
49457
- sourceReport: { status: "assignment-status" }
49458
- }]
49459
- };
49460
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
49461
- var powerSourceCapability = {
49462
- name: "power-source",
49463
- scope: "device",
49464
- mode: "singleton",
49465
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49466
- methods: {
49467
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
49468
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49469
- kind: "mutation",
49470
- auth: "admin"
49471
- }),
49472
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49473
- kind: "mutation",
49474
- auth: "admin"
49475
- }),
49476
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49477
- kind: "mutation",
49478
- auth: "admin"
49479
- })
49480
- },
49481
- runtimeState: PowerSourceStateSchema,
49482
- customize: powerSourceCustomize,
49483
- /**
49484
- * Runtime-state durability: **session** — mirrored from the linked switch on
49485
- * its next report; a restored value would outlive the reading it names.
49486
- *
49487
- * See `RuntimeStateDurability`. Enforced by
49488
- * `scripts/check-runtime-state-durability.ts`.
49489
- */
49490
- durability: "session",
49491
- history: "none"
49492
- };
49493
50315
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
49494
50316
  DeviceType.Camera;
49495
50317
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -49517,6 +50339,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
49517
50339
  }).optional()
49518
50340
  })) });
49519
50341
  /**
50342
+ * A saved Customize assignment (D936). The schema is the type: callers use
50343
+ * `z.infer`, they do not restate the shape.
50344
+ */
50345
+ var OverrideSourceSchema = object({
50346
+ addonId: string().min(1),
50347
+ stableId: string().min(1),
50348
+ cap: string().min(1),
50349
+ fieldPath: string().min(1)
50350
+ });
50351
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50352
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50353
+ var ThresholdTransformDraftSchema = object({
50354
+ kind: literal("threshold"),
50355
+ comparison: _enum(["gte", "lte"]),
50356
+ threshold: number(),
50357
+ startDwellMs: number().nonnegative(),
50358
+ stopDwellMs: number().nonnegative()
50359
+ });
50360
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50361
+ IdentityTransformDraftSchema,
50362
+ InvertTransformDraftSchema,
50363
+ ThresholdTransformDraftSchema,
50364
+ object({
50365
+ kind: literal("linear"),
50366
+ scale: number(),
50367
+ offset: number(),
50368
+ clampMin: number().optional(),
50369
+ clampMax: number().optional()
50370
+ }),
50371
+ object({
50372
+ kind: literal("enum-map"),
50373
+ mapping: record(string(), string().nullable())
50374
+ }),
50375
+ object({ kind: literal("number") })
50376
+ ]);
50377
+ var ScalarOverrideDraftSchema = object({
50378
+ kind: literal("scalar"),
50379
+ cap: string().min(1),
50380
+ targetId: string().min(1),
50381
+ source: OverrideSourceSchema,
50382
+ transform: CustomizeTransformDraftSchema
50383
+ });
50384
+ var KeyedArrayOverrideDraftSchema = object({
50385
+ kind: literal("keyedArray"),
50386
+ cap: string().min(1),
50387
+ targetId: string().min(1),
50388
+ key: string().min(1),
50389
+ label: string().min(1),
50390
+ fieldPath: string().min(1),
50391
+ source: OverrideSourceSchema,
50392
+ transform: CustomizeTransformDraftSchema
50393
+ });
50394
+ var PulseOverrideDraftSchema = object({
50395
+ kind: literal("pulse"),
50396
+ cap: string().min(1),
50397
+ targetId: string().min(1),
50398
+ enabled: boolean(),
50399
+ debounceMs: number().nonnegative(),
50400
+ edges: array(_enum([
50401
+ "rising",
50402
+ "button",
50403
+ "native-event"
50404
+ ])).min(1).optional(),
50405
+ sources: array(OverrideSourceSchema).min(1)
50406
+ });
50407
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50408
+ "in-addition",
50409
+ "confirm",
50410
+ "replace"
50411
+ ]).optional() });
50412
+ discriminatedUnion("kind", [
50413
+ ScalarOverrideDraftSchema,
50414
+ KeyedArrayOverrideDraftSchema,
50415
+ PulseOverrideDraftSchema,
50416
+ object({
50417
+ kind: literal("span"),
50418
+ cap: string().min(1),
50419
+ targetId: string().min(1),
50420
+ timeoutMs: number().nonnegative(),
50421
+ cooldownMs: number().nonnegative().optional(),
50422
+ sources: array(SpanSourceDraftSchema)
50423
+ })
50424
+ ]);
50425
+ object({
50426
+ targetId: string().min(1),
50427
+ configured: boolean(),
50428
+ source: _enum(["native", "assigned"]),
50429
+ state: _enum([
50430
+ "active",
50431
+ "stale",
50432
+ "released",
50433
+ "unavailable"
50434
+ ]),
50435
+ reason: string().nullable()
50436
+ });
50437
+ /**
50438
+ * What a running-threshold proposal says (planning#405). The schema is the
50439
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50440
+ *
50441
+ * A proposal is read-only advice: it carries the numbers it was derived from
50442
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50443
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50444
+ */
50445
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50446
+ var RunningProposalConfidenceSchema = _enum([
50447
+ "high",
50448
+ "medium",
50449
+ "low"
50450
+ ]);
50451
+ var RunningProposalInsufficientReasonSchema = _enum([
50452
+ "no-history",
50453
+ "span-too-short",
50454
+ "truncated",
50455
+ "no-idle-floor",
50456
+ "no-runs",
50457
+ "no-separation"
50458
+ ]);
50459
+ var RunningProposalReplaySchema = object({
50460
+ /** Intervals the real dwell expression held true over the window. */
50461
+ intervals: number().int().nonnegative(),
50462
+ /** Intervals outside every run, or a second interval inside one. */
50463
+ extra: number().int().nonnegative(),
50464
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50465
+ missed: number().int().nonnegative()
50466
+ });
50467
+ var RunningProposalEvidenceSchema = object({
50468
+ tier: RunningProposalTierSchema,
50469
+ spanMs: number().nonnegative(),
50470
+ from: number(),
50471
+ to: number(),
50472
+ points: number().int().nonnegative(),
50473
+ runsFound: number().int().nonnegative(),
50474
+ pausesFound: number().int().nonnegative(),
50475
+ longestPauseMs: number().nonnegative(),
50476
+ idleFloorW: number(),
50477
+ noiseCeilingW: number(),
50478
+ plateauW: number(),
50479
+ replay: RunningProposalReplaySchema
50480
+ });
50481
+ var RunningProposalInsufficientEvidenceSchema = object({
50482
+ spanMs: number().nonnegative(),
50483
+ points: number().int().nonnegative(),
50484
+ runsFound: number().int().nonnegative()
50485
+ });
50486
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50487
+ state: literal("proposal"),
50488
+ transform: ThresholdTransformDraftSchema,
50489
+ evidence: RunningProposalEvidenceSchema,
50490
+ confidence: RunningProposalConfidenceSchema,
50491
+ /** Why the confidence is not higher; empty when nothing held it back. */
50492
+ notes: array(string())
50493
+ }), object({
50494
+ state: literal("insufficient"),
50495
+ reason: RunningProposalInsufficientReasonSchema,
50496
+ evidence: RunningProposalInsufficientEvidenceSchema,
50497
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50498
+ fallback: ThresholdTransformDraftSchema
50499
+ })]);
50500
+ /**
49520
50501
  * The report of a one-shot "move this owner's plaintext secrets into the
49521
50502
  * keychain" run (D878). COUNTS only - never a value, never a ref.
49522
50503
  *
@@ -49806,6 +50787,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49806
50787
  kind: "mutation",
49807
50788
  auth: "admin",
49808
50789
  access: "view"
50790
+ }), method(object({
50791
+ deviceId: number().int(),
50792
+ cap: string().min(1),
50793
+ fieldPath: string().min(1),
50794
+ deviceType: _enum(DeviceType).optional()
50795
+ }), RunningThresholdProposalSchema, {
50796
+ auth: "admin",
50797
+ access: "view"
49809
50798
  }), method(object({
49810
50799
  blockId: string().min(1),
49811
50800
  trace: AutomationRunTraceSchema.optional(),
@@ -50782,7 +51771,27 @@ method(object({
50782
51771
  * exposes only the COMMON surface: link state, supported device
50783
51772
  * kinds (so the UI can filter the device picker), and per-device
50784
51773
  * expose/unexpose.
50785
- */
51774
+ *
51775
+ * INSTANCES. One export addon can serve several independent targets (a
51776
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
51777
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
51778
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
51779
+ * default instance's); present = that instance; an id the provider does not
51780
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
51781
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
51782
+ * change always names the target it changes. A single-instance provider
51783
+ * accepts an absent id on a read or its own id, and refuses any other.
51784
+ */
51785
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
51786
+ var InstanceIdSchema$1 = string().min(1);
51787
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
51788
+ /** One independent export target of an addon. */
51789
+ var ExportInstanceSchema = object({
51790
+ instanceId: InstanceIdSchema$1,
51791
+ name: string(),
51792
+ enabled: boolean(),
51793
+ kind: string().optional()
51794
+ });
50786
51795
  var LinkStateSchema = _enum([
50787
51796
  "unlinked",
50788
51797
  "linked",
@@ -50833,10 +51842,14 @@ var ExposedDeviceSchema = object({
50833
51842
  capabilities: array(string()).optional()
50834
51843
  });
50835
51844
  var ExposeInputSchema = object({
51845
+ instanceId: InstanceIdSchema$1,
50836
51846
  deviceId: string(),
50837
51847
  capabilities: array(string()).optional()
50838
51848
  });
50839
- var UnexposeInputSchema = object({ deviceId: string() });
51849
+ var UnexposeInputSchema = object({
51850
+ instanceId: InstanceIdSchema$1,
51851
+ deviceId: string()
51852
+ });
50840
51853
  /**
50841
51854
  * What an exporter may expose besides devices (D968). An action and a
50842
51855
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -50865,15 +51878,21 @@ var ExposedEntitySchema = object({
50865
51878
  entity: ExportEntityRefSchema,
50866
51879
  exposedAs: string().optional()
50867
51880
  });
50868
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50869
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50870
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51881
+ var ExposeEntityInputSchema = object({
51882
+ instanceId: InstanceIdSchema$1,
51883
+ entity: ExportEntityRefSchema
51884
+ });
51885
+ var UnexposeEntityInputSchema = object({
51886
+ instanceId: InstanceIdSchema$1,
51887
+ entity: ExportEntityRefSchema
51888
+ });
51889
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50871
51890
  kind: "mutation",
50872
51891
  auth: "admin"
50873
51892
  }), method(UnexposeInputSchema, _void(), {
50874
51893
  kind: "mutation",
50875
51894
  auth: "admin"
50876
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51895
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50877
51896
  kind: "mutation",
50878
51897
  auth: "admin"
50879
51898
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -54464,15 +55483,15 @@ var FanPresetCaptureResultSchema = object({
54464
55483
  items: array(FanCaptureItemSchema),
54465
55484
  omitted: array(FanCaptureOmissionSchema)
54466
55485
  });
54467
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
55486
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
54468
55487
  DeviceType.Fan;
54469
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55488
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
54470
55489
  kind: "mutation",
54471
55490
  auth: "admin"
54472
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55491
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
54473
55492
  kind: "mutation",
54474
55493
  auth: "protected"
54475
- }), method(deviceInput$1.extend({
55494
+ }), method(deviceInput$2.extend({
54476
55495
  fields: array(FanCaptureFieldSchema).optional(),
54477
55496
  steps: array(ActionStepSchema).max(20).default([])
54478
55497
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -55355,6 +56374,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55355
56374
  }), object({ success: boolean() }), {
55356
56375
  kind: "mutation",
55357
56376
  auth: "admin"
56377
+ }), method(object({ id: string() }), object({
56378
+ rows: number().int().nonnegative(),
56379
+ secrets: number().int().nonnegative()
56380
+ }), {
56381
+ kind: "mutation",
56382
+ auth: "admin"
55358
56383
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
55359
56384
  addonId: string(),
55360
56385
  settings: record(string(), unknown())
@@ -55383,13 +56408,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55383
56408
  */
55384
56409
  var ConfigSchemaPassthrough$3 = custom();
55385
56410
  var IntegrationInstanceIdSchema = string().min(1);
56411
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56412
+ /**
56413
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56414
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56415
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56416
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56417
+ * instance id.
56418
+ */
56419
+ var IntegrationInstanceKindSchema = object({
56420
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56421
+ id: string().min(1),
56422
+ label: string().min(1),
56423
+ /** The kind signs in through a server-held interactive session. */
56424
+ signIn: object({
56425
+ basePath: string().min(1),
56426
+ methods: array(_enum(["device-code", "proxy"])).optional()
56427
+ }).optional(),
56428
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56429
+ connectPath: string().min(1).optional(),
56430
+ /** Relative hub path to disconnect that account. */
56431
+ disconnectPath: string().min(1).optional()
56432
+ });
55386
56433
  var IntegrationInstanceDescriptionSchema = object({
56434
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56435
+ role: _enum([
56436
+ "devices",
56437
+ "services",
56438
+ "both"
56439
+ ]),
56440
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56441
+ discovery: object({
56442
+ mode: _enum([
56443
+ "none",
56444
+ "lan",
56445
+ "account"
56446
+ ]),
56447
+ scopedToInstance: boolean()
56448
+ }).optional(),
55387
56449
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55388
- instanceMode: _enum(["single", "multiple"]),
56450
+ instanceMode: IntegrationInstanceModeSchema,
55389
56451
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55390
56452
  kind: string(),
55391
56453
  /** True when `testInstance` really probes the remote. */
55392
- canTest: boolean()
56454
+ canTest: boolean(),
56455
+ /**
56456
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56457
+ * Add flow asks which first and sends it as `createInstance.kind`.
56458
+ */
56459
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56460
+ /**
56461
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56462
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56463
+ */
56464
+ allowRestoreId: boolean().optional(),
56465
+ /**
56466
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56467
+ * send an id (the UI then mints an opaque one).
56468
+ */
56469
+ mintsId: boolean().optional()
55393
56470
  });
55394
56471
  var IntegrationInstanceStatusSchema = object({
55395
56472
  state: _enum([
@@ -55398,7 +56475,9 @@ var IntegrationInstanceStatusSchema = object({
55398
56475
  "error",
55399
56476
  "unknown"
55400
56477
  ]),
55401
- detail: string().optional()
56478
+ detail: string().optional(),
56479
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56480
+ label: string().optional()
55402
56481
  });
55403
56482
  var IntegrationInstanceSchema = object({
55404
56483
  id: IntegrationInstanceIdSchema,
@@ -55406,13 +56485,18 @@ var IntegrationInstanceSchema = object({
55406
56485
  /** The addon's own rule (an URL is present, a client id is present...). */
55407
56486
  configured: boolean(),
55408
56487
  /**
55409
- * The instance's own on/off switch (D996): always present, derived from the record body
55410
- * (`enabled !== false`, so a row written before the field existed is on).
56488
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56489
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56490
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
55411
56491
  */
55412
- enabled: boolean(),
56492
+ enabled: boolean().optional(),
56493
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56494
+ kind: string().optional(),
55413
56495
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55414
56496
  formValues: record(string(), unknown()),
55415
- status: IntegrationInstanceStatusSchema.optional()
56497
+ status: IntegrationInstanceStatusSchema.optional(),
56498
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56499
+ lastCheckedAt: number().optional()
55416
56500
  });
55417
56501
  var IntegrationInstancePatchSchema = object({
55418
56502
  id: IntegrationInstanceIdSchema,
@@ -55420,31 +56504,57 @@ var IntegrationInstancePatchSchema = object({
55420
56504
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55421
56505
  values: record(string(), unknown())
55422
56506
  });
55423
- 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({
55424
- id: IntegrationInstanceIdSchema,
55425
- name: string().min(1)
55426
- }), object({ id: string() }), {
55427
- kind: "mutation",
55428
- auth: "admin"
55429
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55430
- kind: "mutation",
55431
- auth: "admin"
55432
- }), method(object({
55433
- id: IntegrationInstanceIdSchema,
55434
- enabled: boolean()
55435
- }), object({ ok: literal(true) }), {
55436
- kind: "mutation",
55437
- auth: "admin"
55438
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55439
- kind: "mutation",
55440
- auth: "admin"
55441
- }), optionalMethod(object({
55442
- id: IntegrationInstanceIdSchema.nullable(),
55443
- values: record(string(), unknown())
55444
- }), TestConnectionResultSchema$1, {
55445
- kind: "mutation",
55446
- auth: "admin"
55447
- });
56507
+ var integrationInstancesCapability = {
56508
+ name: "integration-instances",
56509
+ scope: "system",
56510
+ mode: "collection",
56511
+ methods: {
56512
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56513
+ /** The form; `id: null` is the create form. */
56514
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56515
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56516
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56517
+ /**
56518
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56519
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56520
+ */
56521
+ createInstance: method(object({
56522
+ id: IntegrationInstanceIdSchema.optional(),
56523
+ name: string().min(1),
56524
+ kind: string().min(1).optional()
56525
+ }), object({ id: string() }), {
56526
+ kind: "mutation",
56527
+ auth: "admin"
56528
+ }),
56529
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56530
+ kind: "mutation",
56531
+ auth: "admin"
56532
+ }),
56533
+ /**
56534
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56535
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56536
+ */
56537
+ setInstanceEnabled: method(object({
56538
+ id: IntegrationInstanceIdSchema,
56539
+ enabled: boolean()
56540
+ }), object({ ok: literal(true) }), {
56541
+ kind: "mutation",
56542
+ auth: "admin"
56543
+ }),
56544
+ /** Delete the record and its keychain refs together. */
56545
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56546
+ kind: "mutation",
56547
+ auth: "admin"
56548
+ }),
56549
+ testInstance: optionalMethod(object({
56550
+ id: IntegrationInstanceIdSchema.nullable(),
56551
+ values: record(string(), unknown())
56552
+ }), TestConnectionResultSchema$1, {
56553
+ kind: "mutation",
56554
+ auth: "admin"
56555
+ })
56556
+ }
56557
+ };
55448
56558
  /**
55449
56559
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55450
56560
  * session (server-initiated offer pattern, identical to the main
@@ -57462,6 +58572,11 @@ var LogEntrySchema = object({
57462
58572
  * flight, and not asking for the next slice ends the scan.
57463
58573
  */
57464
58574
  var LogReadQuerySchema = object({
58575
+ /**
58576
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58577
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58578
+ */
58579
+ instanceId: string().min(1).max(200).optional(),
57465
58580
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
57466
58581
  requestId: string().min(1).max(64).optional(),
57467
58582
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -57524,6 +58639,8 @@ var LogReadResultSchema = object({
57524
58639
  /** True when `cancelRead` ended this slice early. */
57525
58640
  cancelled: boolean().optional()
57526
58641
  });
58642
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58643
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
57527
58644
  var LogReadCapabilitiesSchema = object({
57528
58645
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
57529
58646
  readable: boolean(),
@@ -57542,7 +58659,7 @@ var LogReadCapabilitiesSchema = object({
57542
58659
  /** How far back the store reaches, when the provider knows (days). */
57543
58660
  retentionDays: number().nullable()
57544
58661
  });
57545
- method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(_void(), LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
58662
+ method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
57546
58663
  kind: "mutation",
57547
58664
  auth: "admin"
57548
58665
  });
@@ -69869,6 +70986,14 @@ var powerMeterCapability = {
69869
70986
  deviceNative: true,
69870
70987
  mode: "singleton",
69871
70988
  deviceTypes: POWER_METER_DEVICE_TYPES,
70989
+ deviceConfig: { ui: {
70990
+ kind: "widget",
70991
+ widgetId: "host/power-meter-line",
70992
+ tab: "power",
70993
+ placement: "hero",
70994
+ label: "Power",
70995
+ order: 4
70996
+ } },
69872
70997
  methods: {},
69873
70998
  status: {
69874
70999
  schema: PowerMeterStatusSchema,
@@ -69928,6 +71053,117 @@ var powerMeterCapability = {
69928
71053
  volatileStateFields: ["lastFetchedAt"],
69929
71054
  customize: powerMeterCustomize
69930
71055
  };
71056
+ var PowerSourceRefusalReasonSchema = _enum([
71057
+ "no-power-source",
71058
+ "switch-missing",
71059
+ "switch-offline",
71060
+ "switch-unknown",
71061
+ "restart-in-progress",
71062
+ "switch-call-failed"
71063
+ ]);
71064
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
71065
+ accepted: literal(false),
71066
+ reason: PowerSourceRefusalReasonSchema,
71067
+ /** One line for the operator, naming the switch. */
71068
+ message: string()
71069
+ })]);
71070
+ var PowerSourceInfoSchema = object({
71071
+ /** False when the device has no power source linked; the rest is then empty. */
71072
+ linked: boolean(),
71073
+ switchDeviceId: number().nullable(),
71074
+ switchName: string().nullable(),
71075
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
71076
+ switchOn: boolean().nullable(),
71077
+ switchOnline: boolean().nullable(),
71078
+ restartInProgress: boolean(),
71079
+ offSeconds: number().int()
71080
+ });
71081
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
71082
+ var PowerSourceStateSchema = object({
71083
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
71084
+ switchOn: boolean().nullable(),
71085
+ offSeconds: number().int(),
71086
+ lastFetchedAt: number()
71087
+ });
71088
+ var POWER_SOURCE_DEVICE_TYPES = [
71089
+ DeviceType.Gateway,
71090
+ DeviceType.Hub,
71091
+ DeviceType.Camera,
71092
+ DeviceType.Sensor,
71093
+ DeviceType.MediaPlayer,
71094
+ DeviceType.Vacuum,
71095
+ DeviceType.Generic,
71096
+ DeviceType.Washer,
71097
+ DeviceType.Dryer,
71098
+ DeviceType.Dishwasher,
71099
+ DeviceType.PetFountain,
71100
+ DeviceType.Fan,
71101
+ DeviceType.Humidifier,
71102
+ DeviceType.Climate,
71103
+ DeviceType.WaterHeater
71104
+ ];
71105
+ var powerSourceCustomize = {
71106
+ version: 1,
71107
+ targets: [{
71108
+ id: "switch",
71109
+ cardinality: "scalar",
71110
+ vocabulary: {
71111
+ cap: "power-source",
71112
+ stateField: "switchOn"
71113
+ },
71114
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71115
+ apply: "add",
71116
+ path: "switchOn",
71117
+ sources: [{
71118
+ kind: "switch",
71119
+ transforms: [{ kind: "identity" }]
71120
+ }],
71121
+ companions: [{
71122
+ kind: "constant",
71123
+ path: "offSeconds",
71124
+ value: 10,
71125
+ when: "add"
71126
+ }, {
71127
+ kind: "clock",
71128
+ path: "lastFetchedAt",
71129
+ when: "always"
71130
+ }],
71131
+ sourceReport: { status: "assignment-status" }
71132
+ }]
71133
+ };
71134
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
71135
+ var powerSourceCapability = {
71136
+ name: "power-source",
71137
+ scope: "device",
71138
+ mode: "singleton",
71139
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71140
+ methods: {
71141
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
71142
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71143
+ kind: "mutation",
71144
+ auth: "admin"
71145
+ }),
71146
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71147
+ kind: "mutation",
71148
+ auth: "admin"
71149
+ }),
71150
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71151
+ kind: "mutation",
71152
+ auth: "admin"
71153
+ })
71154
+ },
71155
+ runtimeState: PowerSourceStateSchema,
71156
+ customize: powerSourceCustomize,
71157
+ /**
71158
+ * Runtime-state durability: **session** — mirrored from the linked switch on
71159
+ * its next report; a restored value would outlive the reading it names.
71160
+ *
71161
+ * See `RuntimeStateDurability`. Enforced by
71162
+ * `scripts/check-runtime-state-durability.ts`.
71163
+ */
71164
+ durability: "session",
71165
+ history: "none"
71166
+ };
69931
71167
  /**
69932
71168
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
69933
71169
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -79407,6 +80643,25 @@ function buildDescriptor(capName, kind) {
79407
80643
  * contributes one entry per bound cap present in this map.
79408
80644
  */
79409
80645
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
80646
+ /**
80647
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
80648
+ * shared by the admin UI and the viewer. A role is data on the wire; each
80649
+ * client resolves its name from `label` (English and Italian side by side) and
80650
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
80651
+ * server sends an id, a client maps it to its own icon set.
80652
+ *
80653
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
80654
+ * pictogram): a client that can draw text shows it, one that cannot falls back
80655
+ * to `icon`.
80656
+ */
80657
+ function digit(n) {
80658
+ return {
80659
+ label: t(n, n),
80660
+ icon: "hash",
80661
+ legend: n
80662
+ };
80663
+ }
80664
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
79410
80665
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
79411
80666
  var btn = (idSuffix, row, col, init) => ({
79412
80667
  idSuffix,
@@ -79534,6 +80789,34 @@ discriminatedUnion("kind", [object({
79534
80789
  kind: literal("pronto"),
79535
80790
  code: string()
79536
80791
  })]);
80792
+ /**
80793
+ * Propose a running threshold from a device's own power history (planning#405).
80794
+ *
80795
+ * Pure and deterministic: the same series gives the same answer. The series is
80796
+ * a HELD-VALUE step series — a point's value stands until the next point — which
80797
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
80798
+ * count of samples.
80799
+ *
80800
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80801
+ * near F. A device that is not near one level most of the time has no floor.
80802
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80803
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80804
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80805
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80806
+ * grid a person would type; stop dwell covers the longest pause; start dwell
80807
+ * covers the longest standby blip above T.
80808
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80809
+ * run must hold exactly one true interval and nothing may flap outside.
80810
+ */
80811
+ var SECOND_MS = 1e3;
80812
+ var MINUTE_MS$1 = 60 * SECOND_MS;
80813
+ 24 * (60 * MINUTE_MS$1);
80814
+ 10 * MINUTE_MS$1;
80815
+ 30 * SECOND_MS;
80816
+ 60 * SECOND_MS;
80817
+ 20 * MINUTE_MS$1;
80818
+ 10 * SECOND_MS;
80819
+ 10 * SECOND_MS;
79537
80820
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79538
80821
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79539
80822
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -82639,6 +83922,13 @@ Object.freeze({
82639
83922
  access: "create",
82640
83923
  role: "admin"
82641
83924
  },
83925
+ "coreBlocks.proposeRunningThreshold": {
83926
+ capName: "core-blocks",
83927
+ capScope: "system",
83928
+ addonId: null,
83929
+ access: "view",
83930
+ role: "admin"
83931
+ },
82642
83932
  "coreBlocks.reportAutomationRun": {
82643
83933
  capName: "core-blocks",
82644
83934
  capScope: "system",
@@ -83150,6 +84440,13 @@ Object.freeze({
83150
84440
  access: "view",
83151
84441
  role: "admin"
83152
84442
  },
84443
+ "deviceExport.listInstances": {
84444
+ capName: "device-export",
84445
+ capScope: "system",
84446
+ addonId: null,
84447
+ access: "view",
84448
+ role: "admin"
84449
+ },
83153
84450
  "deviceExport.listSupportedDeviceKinds": {
83154
84451
  capName: "device-export",
83155
84452
  capScope: "system",
@@ -84802,6 +86099,13 @@ Object.freeze({
84802
86099
  access: "create",
84803
86100
  role: "admin"
84804
86101
  },
86102
+ "integrations.clearSettings": {
86103
+ capName: "integrations",
86104
+ capScope: "system",
86105
+ addonId: null,
86106
+ access: "delete",
86107
+ role: "admin"
86108
+ },
84805
86109
  "integrations.create": {
84806
86110
  capName: "integrations",
84807
86111
  capScope: "system",
@@ -92097,6 +93401,11 @@ Object.freeze({
92097
93401
  form: "single",
92098
93402
  optional: false
92099
93403
  }],
93404
+ "coreBlocks.proposeRunningThreshold": [{
93405
+ name: "deviceId",
93406
+ form: "single",
93407
+ optional: false
93408
+ }],
92100
93409
  "cover.close": [{
92101
93410
  name: "deviceId",
92102
93411
  form: "single",
@@ -94760,99 +96069,6 @@ Object.freeze({
94760
96069
  "network-access": "ingress",
94761
96070
  "smtp-provider": "email"
94762
96071
  });
94763
- /**
94764
- * A saved Customize assignment (D936). The schema is the type: callers use
94765
- * `z.infer`, they do not restate the shape.
94766
- */
94767
- var OverrideSourceSchema = object({
94768
- addonId: string().min(1),
94769
- stableId: string().min(1),
94770
- cap: string().min(1),
94771
- fieldPath: string().min(1)
94772
- });
94773
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
94774
- object({ kind: literal("identity") }),
94775
- object({ kind: literal("invert") }),
94776
- object({
94777
- kind: literal("threshold"),
94778
- comparison: _enum(["gte", "lte"]),
94779
- threshold: number(),
94780
- startDwellMs: number().nonnegative(),
94781
- stopDwellMs: number().nonnegative()
94782
- }),
94783
- object({
94784
- kind: literal("linear"),
94785
- scale: number(),
94786
- offset: number(),
94787
- clampMin: number().optional(),
94788
- clampMax: number().optional()
94789
- }),
94790
- object({
94791
- kind: literal("enum-map"),
94792
- mapping: record(string(), string().nullable())
94793
- }),
94794
- object({ kind: literal("number") })
94795
- ]);
94796
- var ScalarOverrideDraftSchema = object({
94797
- kind: literal("scalar"),
94798
- cap: string().min(1),
94799
- targetId: string().min(1),
94800
- source: OverrideSourceSchema,
94801
- transform: CustomizeTransformDraftSchema
94802
- });
94803
- var KeyedArrayOverrideDraftSchema = object({
94804
- kind: literal("keyedArray"),
94805
- cap: string().min(1),
94806
- targetId: string().min(1),
94807
- key: string().min(1),
94808
- label: string().min(1),
94809
- fieldPath: string().min(1),
94810
- source: OverrideSourceSchema,
94811
- transform: CustomizeTransformDraftSchema
94812
- });
94813
- var PulseOverrideDraftSchema = object({
94814
- kind: literal("pulse"),
94815
- cap: string().min(1),
94816
- targetId: string().min(1),
94817
- enabled: boolean(),
94818
- debounceMs: number().nonnegative(),
94819
- edges: array(_enum([
94820
- "rising",
94821
- "button",
94822
- "native-event"
94823
- ])).min(1).optional(),
94824
- sources: array(OverrideSourceSchema).min(1)
94825
- });
94826
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
94827
- "in-addition",
94828
- "confirm",
94829
- "replace"
94830
- ]).optional() });
94831
- discriminatedUnion("kind", [
94832
- ScalarOverrideDraftSchema,
94833
- KeyedArrayOverrideDraftSchema,
94834
- PulseOverrideDraftSchema,
94835
- object({
94836
- kind: literal("span"),
94837
- cap: string().min(1),
94838
- targetId: string().min(1),
94839
- timeoutMs: number().nonnegative(),
94840
- cooldownMs: number().nonnegative().optional(),
94841
- sources: array(SpanSourceDraftSchema)
94842
- })
94843
- ]);
94844
- object({
94845
- targetId: string().min(1),
94846
- configured: boolean(),
94847
- source: _enum(["native", "assigned"]),
94848
- state: _enum([
94849
- "active",
94850
- "stale",
94851
- "released",
94852
- "unavailable"
94853
- ]),
94854
- reason: string().nullable()
94855
- });
94856
96072
  var G711_SCALE_CORRECTION_DB = {
94857
96073
  PCMU: 20 * Math.log10(4),
94858
96074
  PCMA: 20 * Math.log10(8)
@@ -96357,6 +97573,20 @@ var VeluxReconciler = class {
96357
97573
  }
96358
97574
  };
96359
97575
  //#endregion
97576
+ //#region src/custom-actions.ts
97577
+ /** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
97578
+ var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
97579
+ hosts: array(object({
97580
+ host: string(),
97581
+ name: string(),
97582
+ id: string()
97583
+ })),
97584
+ summary: string()
97585
+ }), {
97586
+ kind: "mutation",
97587
+ auth: "admin"
97588
+ }) });
97589
+ //#endregion
96360
97590
  //#region src/config.ts
96361
97591
  /**
96362
97592
  * Operator-supplied connection to ONE Velux KLF-200 gateway on the local
@@ -96480,20 +97710,6 @@ function buildConnectionFormSchema() {
96480
97710
  }] };
96481
97711
  }
96482
97712
  //#endregion
96483
- //#region src/custom-actions.ts
96484
- /** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
96485
- var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
96486
- hosts: array(object({
96487
- host: string(),
96488
- name: string(),
96489
- id: string()
96490
- })),
96491
- summary: string()
96492
- }), {
96493
- kind: "mutation",
96494
- auth: "admin"
96495
- }) });
96496
- //#endregion
96497
97713
  //#region src/velux-node-resolver.ts
96498
97714
  /**
96499
97715
  * Per-integration registry that device classes use to reach the live
@@ -105815,7 +107031,54 @@ function veluxDiscoverySummary(foundCount, proposedCount) {
105815
107031
  return `Found ${proposedCount} ${proposedCount === 1 ? "gateway" : "gateways"}.`;
105816
107032
  }
105817
107033
  //#endregion
107034
+ //#region src/velux-instances.ts
107035
+ /**
107036
+ * Velux integrations as `integration-instances` (planning#416, D1007): one `connections` record
107037
+ * per integration under its registry id, holding the connection to ONE KLF-200 gateway. The
107038
+ * gateway password is declared a secret by type; the records door keeps it in the keychain.
107039
+ */
107040
+ var veluxConnectionsEntryKind = defineEntryKind("connections", {
107041
+ title: "Velux gateways",
107042
+ schema: entryBody({
107043
+ host: string().optional(),
107044
+ password: string().optional().meta({ secret: true })
107045
+ }),
107046
+ writeTrigger: "an operator adds, edits or removes a Velux KLF-200 gateway integration"
107047
+ });
107048
+ /** Every kind this addon declares, keyed by its name. */
107049
+ var veluxRecordKinds = { connections: veluxConnectionsEntryKind };
107050
+ function buildVeluxInstances(records, deps) {
107051
+ return new RecordIntegrationInstances(records, veluxConnectionsEntryKind, {
107052
+ description: {
107053
+ role: "devices",
107054
+ discovery: {
107055
+ mode: "account",
107056
+ scopedToInstance: true
107057
+ },
107058
+ instanceMode: "multiple",
107059
+ kind: "server",
107060
+ canTest: false
107061
+ },
107062
+ formSchema: () => buildConnectionFormSchema(),
107063
+ initialBody: (name) => ({ name }),
107064
+ configured: (body) => (body.host ?? "").length > 0 && body.password !== void 0,
107065
+ onChanged: () => deps.onChanged()
107066
+ });
107067
+ }
107068
+ /**
107069
+ * The gateway connection of one integration as the addon's runtime reads it: from the record,
107070
+ * never from the legacy rows. Null while the record is absent, incomplete, or its password is
107071
+ * unreadable (the runtime keeps what it had, D49).
107072
+ */
107073
+ async function veluxConnectionOf(instances, id) {
107074
+ const read = await instances.readResolved(id);
107075
+ if (read === null || read.secrets === "unavailable") return null;
107076
+ return connectionFromSettings(read.body);
107077
+ }
107078
+ //#endregion
105818
107079
  //#region src/addon.ts
107080
+ /** The date of the integration-rows-to-record migration (D1007). */
107081
+ var VELUX_MIGRATIONS_DATE = "2026-11-05";
105819
107082
  /**
105820
107083
  * Velux KLF-200 — `mode: account`, one integration per gateway.
105821
107084
  *
@@ -105838,11 +107101,27 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105838
107101
  };
105839
107102
  integrationReconciler = null;
105840
107103
  clients = null;
107104
+ instances = null;
105841
107105
  constructor() {
105842
107106
  super({});
105843
107107
  }
105844
107108
  async onInitialize() {
105845
107109
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
107110
+ const records = this.ctx.records;
107111
+ if (records === void 0) throw new Error("provider-velux: ctx.records is required (hub-only addon)");
107112
+ await records.declare(veluxRecordKinds);
107113
+ await this.runMigrations(defineAddonMigrations([integrationRowsMigration({
107114
+ addonId: this.addonId,
107115
+ date: VELUX_MIGRATIONS_DATE,
107116
+ kind: veluxConnectionsEntryKind,
107117
+ fixtureMark: "host"
107118
+ })], integrationRowsPortsOver({
107119
+ api: defaultIntegrationApiOver(this.ctx.api),
107120
+ records,
107121
+ kind: veluxConnectionsEntryKind,
107122
+ warn: (message) => this.ctx.logger.warn(message)
107123
+ })));
107124
+ this.instances = buildVeluxInstances(records, { onChanged: () => this.reconcileIntegrations() });
105846
107125
  setKlfLibraryLogSink((level, message) => {
105847
107126
  this.ctx.logger.debug(`klf-200-api ${level}: ${message}`);
105848
107127
  });
@@ -105868,6 +107147,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105868
107147
  {
105869
107148
  capability: connectionTestCapability,
105870
107149
  provider: this.buildConnectionTestProvider()
107150
+ },
107151
+ {
107152
+ capability: integrationInstancesCapability,
107153
+ provider: this.requireInstances().toProvider()
105871
107154
  }
105872
107155
  ],
105873
107156
  customActions: veluxActions,
@@ -105902,9 +107185,6 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105902
107185
  setKlfLibraryLogSink(null);
105903
107186
  await super.onShutdown();
105904
107187
  }
105905
- globalSettingsSchema() {
105906
- return buildConnectionFormSchema();
105907
- }
105908
107188
  async supportsManualCreation() {
105909
107189
  return false;
105910
107190
  }
@@ -105946,7 +107226,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105946
107226
  for (const integration of all) {
105947
107227
  if (integration.addonId !== this.ctx.id) continue;
105948
107228
  try {
105949
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
107229
+ const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
105950
107230
  if (connection) hosts.push(connection.host);
105951
107231
  } catch (err) {
105952
107232
  this.ctx.logger.warn("Velux discovery: settings read failed", {
@@ -105961,6 +107241,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105961
107241
  return [];
105962
107242
  }
105963
107243
  }
107244
+ requireInstances() {
107245
+ if (!this.instances) throw new Error("Velux provider not initialised");
107246
+ return this.instances;
107247
+ }
105964
107248
  requireClients() {
105965
107249
  if (!this.clients) throw new Error("Velux provider not initialised");
105966
107250
  return this.clients;
@@ -105977,7 +107261,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105977
107261
  },
105978
107262
  listIntegrations: () => this.ctx.api.integrations.list.query(),
105979
107263
  applySettings: async (integration) => {
105980
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
107264
+ const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
105981
107265
  if (!connection) return false;
105982
107266
  await this.requireClients().upsert(integration.id, integration.name, connection);
105983
107267
  return true;