@camstack/addon-import-alexa 0.2.182 → 0.2.183

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
@@ -6042,7 +6042,7 @@ function number(params) {
6042
6042
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6043
6043
  }
6044
6044
  //#endregion
6045
- //#region ../types/dist/sleep-DV4qxEyD.mjs
6045
+ //#region ../types/dist/sleep-BkzQdpzF.mjs
6046
6046
  /**
6047
6047
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6048
6048
  * window to float samples (D455).
@@ -6190,9 +6190,16 @@ function isSecretMarker(value) {
6190
6190
  const ref = value["$secret"];
6191
6191
  return typeof ref === "string" && ref.length > 0;
6192
6192
  }
6193
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6194
+ function isSecretMarkerLike(value) {
6195
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6196
+ }
6193
6197
  function isSecretClear(value) {
6194
6198
  return isRecord$7(value) && value["$secretClear"] === true;
6195
6199
  }
6200
+ function isSecretFieldView(value) {
6201
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6202
+ }
6196
6203
  /**
6197
6204
  * The UI view of a stored secret field value: a marker becomes "set", inline
6198
6205
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6842,11 +6849,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6842
6849
  }
6843
6850
  return out;
6844
6851
  }
6852
+ /**
6853
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6854
+ *
6855
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6856
+ * scope (global, one node, one device, one integration, one row of a business
6857
+ * collection) and the field path inside that scope — so saving the same field
6858
+ * twice updates the same ref, and the ref itself says which settings blob
6859
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6860
+ * copied onto another ref does not decrypt.
6861
+ *
6862
+ * Array items are addressed by ITEM ID, never by index
6863
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6864
+ *
6865
+ * Pure and browser-safe: no node builtins, no I/O.
6866
+ */
6867
+ /** The URI scheme of every keychain ref. */
6868
+ var SECRET_REF_SCHEME = "secret://";
6845
6869
  var GLOBAL_SCOPE = { kind: "global" };
6846
6870
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6847
6871
  function globalSecretScope() {
6848
6872
  return GLOBAL_SCOPE;
6849
6873
  }
6874
+ /**
6875
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6876
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6877
+ * addon running forked.
6878
+ */
6879
+ function normalizeSecretOwner(addonId) {
6880
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6881
+ }
6850
6882
  function encodeSegment(value) {
6851
6883
  return encodeURIComponent(value);
6852
6884
  }
@@ -6857,6 +6889,16 @@ function decodeSegment(value) {
6857
6889
  return null;
6858
6890
  }
6859
6891
  }
6892
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6893
+ function formatSecretScope(scope) {
6894
+ switch (scope.kind) {
6895
+ case "global": return "global";
6896
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6897
+ case "device": return `device/${scope.deviceId}`;
6898
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6899
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6900
+ }
6901
+ }
6860
6902
  /**
6861
6903
  * The field path of a value inside an array item, addressed by the item's id:
6862
6904
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6865,6 +6907,13 @@ function decodeSegment(value) {
6865
6907
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6866
6908
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6867
6909
  }
6910
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6911
+ function buildSecretRef(parts) {
6912
+ const owner = normalizeSecretOwner(parts.owner);
6913
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6914
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6915
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6916
+ }
6868
6917
  function parseScope(segments) {
6869
6918
  switch (segments[0]) {
6870
6919
  case "global": return {
@@ -8822,6 +8871,8 @@ var RECORD_BATCH_MAX = 1e3;
8822
8871
  var RECORD_QUERY_MAX = 1e3;
8823
8872
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8824
8873
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8874
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8875
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8825
8876
  /**
8826
8877
  * Secret fields of a record, addressed the way a Zod schema declares them
8827
8878
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
@@ -8858,6 +8909,112 @@ function secretArrayPrefixes(path) {
8858
8909
  }
8859
8910
  return prefixes;
8860
8911
  }
8912
+ function isRecord$4$1(value) {
8913
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8914
+ }
8915
+ function elementId(element, idKey, declared, prefix) {
8916
+ const id = isRecord$4$1(element) ? element[idKey] : void 0;
8917
+ 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`);
8918
+ return id;
8919
+ }
8920
+ /**
8921
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8922
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8923
+ * result is a new body, `data` is untouched.
8924
+ */
8925
+ var REMOVE_LEAF = Symbol("remove-leaf");
8926
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8927
+ if (!isRecord$4$1(node)) return node;
8928
+ const segment = segments[index];
8929
+ if (segment === void 0) return node;
8930
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8931
+ const child = node[segment.key];
8932
+ const keys = [...walkedKeys, segment.key];
8933
+ const last = index === segments.length - 1;
8934
+ if (segment.each) {
8935
+ if (!Array.isArray(child)) return node;
8936
+ const prefix = keys.join(".");
8937
+ const idKey = arrayKeys[prefix];
8938
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8939
+ const mapped = child.map((element) => {
8940
+ const id = elementId(element, idKey, declared, prefix);
8941
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8942
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8943
+ });
8944
+ return {
8945
+ ...node,
8946
+ [segment.key]: mapped
8947
+ };
8948
+ }
8949
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8950
+ if (last) {
8951
+ const replacement = visit({
8952
+ declared,
8953
+ concrete,
8954
+ value: child
8955
+ });
8956
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8957
+ return {
8958
+ ...node,
8959
+ [segment.key]: replacement
8960
+ };
8961
+ }
8962
+ return {
8963
+ ...node,
8964
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8965
+ };
8966
+ }
8967
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8968
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8969
+ let current = data;
8970
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8971
+ return current;
8972
+ }
8973
+ /**
8974
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8975
+ * record's refs never collide with the refs a legacy collection of the same name
8976
+ * already holds during an adoption.
8977
+ */
8978
+ function recordSecretCollection(kind) {
8979
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8980
+ }
8981
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8982
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8983
+ return buildSecretRef({
8984
+ owner,
8985
+ scope: {
8986
+ kind: "row",
8987
+ collection: recordSecretCollection(kind),
8988
+ rowId
8989
+ },
8990
+ fieldPath: concretePath
8991
+ });
8992
+ }
8993
+ /**
8994
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8995
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8996
+ * token. The form hands it to the secret field, which reveals the value by ref
8997
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8998
+ * stored secret (the records door reads a view as KEEP).
8999
+ *
9000
+ * ONE implementation for the entries service and for every addon that serves
9001
+ * its own instances (`RecordIntegrationInstances`).
9002
+ */
9003
+ function isRecord$3$1(value) {
9004
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9005
+ }
9006
+ function secretViewsOf(owner, kind, id, decl, data) {
9007
+ if (decl.secretPaths.length === 0) return data;
9008
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
9009
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
9010
+ return {
9011
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
9012
+ v: 1,
9013
+ set: true
9014
+ };
9015
+ });
9016
+ return isRecord$3$1(shown) ? shown : data;
9017
+ }
8861
9018
  /**
8862
9019
  * The share-view scope kinds a share-link token can carry. The hub mints
8863
9020
  * and validates them (`ShareViewGrantSchema` builds its
@@ -9080,7 +9237,7 @@ function defineRecordKinds(specs) {
9080
9237
  retention: spec.retention,
9081
9238
  deviceRemoval: spec.deviceRemoval,
9082
9239
  maxDataBytes: spec.maxDataBytes,
9083
- secretPaths: [...secretPaths],
9240
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9084
9241
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9085
9242
  transactFamily: spec.transactFamily,
9086
9243
  entry: spec.entry === void 0 ? void 0 : {
@@ -9125,6 +9282,7 @@ function defineEntryKind(name, spec) {
9125
9282
  ...spec.database === void 0 ? {} : { database: spec.database },
9126
9283
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9127
9284
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9285
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9128
9286
  entry: {
9129
9287
  title: spec.title,
9130
9288
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11790,7 +11948,7 @@ object({
11790
11948
  params: record(string(), union([string(), number$1()])).optional()
11791
11949
  });
11792
11950
  //#endregion
11793
- //#region ../types/dist/composition-CD1GdFuv.mjs
11951
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11794
11952
  /**
11795
11953
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11796
11954
  *
@@ -12553,38 +12711,6 @@ function validateExpressionSource(src) {
12553
12711
  return null;
12554
12712
  }
12555
12713
  /**
12556
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12557
- *
12558
- * Standby of the control board is under 5 W; a motor or a heater is far
12559
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12560
- * a cycle, so running starts only after 30 s above the line.
12561
- *
12562
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12563
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12564
- * report "finished" in the middle of the program, so the stop dwell is
12565
- * 10 minutes on every type. The number is stored per type so one machine
12566
- * can move later without moving the others.
12567
- */
12568
- var APPLIANCE_TEMPLATE_ID = "appliance";
12569
- var APPLIANCE_KINDS = [
12570
- "washer",
12571
- "dryer",
12572
- "dishwasher"
12573
- ];
12574
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12575
- /**
12576
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12577
- *
12578
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12579
- * (D967), so the heater switches exactly as the plug does. Running is the
12580
- * plug being on; when a power reading is also named, running is the draw
12581
- * staying above the line instead (a plug left on with the stove's own
12582
- * thermostat open is not heating). 50 W is far above any standby and below
12583
- * the smallest radiator.
12584
- */
12585
- var HEATER_TEMPLATE_ID = "heater";
12586
- DeviceType.Heater;
12587
- /**
12588
12714
  * Statistics and integrals over a history read (D944).
12589
12715
  *
12590
12716
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12728,34 +12854,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12728
12854
  /** Actuator groups only. Readings leave this unset. */
12729
12855
  active: ActiveCriterionSchema.optional()
12730
12856
  }),
12731
- object({
12732
- kind: literal("appliance"),
12733
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12734
- appliance: _enum(APPLIANCE_KINDS),
12735
- thresholdWatts: number$1().positive(),
12736
- startDwellMs: number$1().int().nonnegative(),
12737
- stopDwellMs: number$1().int().nonnegative(),
12738
- member: HelperMemberSchema,
12739
- /**
12740
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12741
- * composition carries a `power-source` feature, so the hub can cut and restore
12742
- * that plug (power-source link of a NEW-target composition).
12743
- */
12744
- powerSwitch: HelperMemberSchema.optional(),
12745
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12746
- offSeconds: number$1().optional()
12747
- }),
12748
- object({
12749
- kind: literal("heater"),
12750
- templateId: literal(HEATER_TEMPLATE_ID),
12751
- /** The plug, read and commanded at `switch.on`. */
12752
- plug: HelperMemberSchema,
12753
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12754
- meter: HelperMemberSchema.optional(),
12755
- thresholdWatts: number$1().positive(),
12756
- startDwellMs: number$1().int().nonnegative(),
12757
- stopDwellMs: number$1().int().nonnegative()
12758
- }),
12759
12857
  object({
12760
12858
  kind: literal("statistic"),
12761
12859
  stat: _enum(STATISTIC_STATS),
@@ -13314,6 +13412,157 @@ function buildCapturingReply(envelope) {
13314
13412
  };
13315
13413
  return wrapper;
13316
13414
  }
13415
+ /** The most instances one addon lists (an integrations page, not a table). */
13416
+ var INSTANCES_LIST_MAX = 1e3;
13417
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13418
+ function topLevelSecretKeys(paths) {
13419
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13420
+ }
13421
+ var RecordIntegrationInstances = class {
13422
+ records;
13423
+ kind;
13424
+ hooks;
13425
+ handle;
13426
+ secretKeys;
13427
+ nameField;
13428
+ constructor(records, kind, hooks) {
13429
+ this.records = records;
13430
+ this.kind = kind;
13431
+ this.hooks = hooks;
13432
+ this.handle = records.kind(kind);
13433
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13434
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13435
+ }
13436
+ /**
13437
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13438
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13439
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13440
+ */
13441
+ async readResolved(id) {
13442
+ try {
13443
+ const record = await this.handle.getResolved(id);
13444
+ return record === null ? null : {
13445
+ id,
13446
+ body: record.data,
13447
+ enabled: record.data.enabled !== false,
13448
+ secrets: "resolved"
13449
+ };
13450
+ } catch (err) {
13451
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13452
+ const stored = await this.handle.get(id);
13453
+ if (stored === null) return null;
13454
+ return {
13455
+ id,
13456
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13457
+ enabled: stored.data.enabled !== false,
13458
+ secrets: "unavailable"
13459
+ };
13460
+ }
13461
+ }
13462
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13463
+ async getResolved(id) {
13464
+ return (await this.readResolved(id))?.body ?? null;
13465
+ }
13466
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13467
+ async listResolved() {
13468
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13469
+ const out = [];
13470
+ for (const record of page.records) {
13471
+ const read = await this.readResolved(record.id);
13472
+ if (read !== null) out.push(read);
13473
+ }
13474
+ return out;
13475
+ }
13476
+ viewOf(record) {
13477
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13478
+ const rawName = record.data[this.nameField];
13479
+ const kind = this.hooks.kindOf?.(record.data);
13480
+ const health = this.hooks.health?.(record.data, record.id);
13481
+ return {
13482
+ id: record.id,
13483
+ name: typeof rawName === "string" ? rawName : record.id,
13484
+ configured: this.hooks.configured(record.data),
13485
+ enabled: record.data.enabled !== false,
13486
+ formValues: shown,
13487
+ ...kind !== void 0 ? { kind } : {},
13488
+ ...health?.status !== void 0 ? { status: health.status } : {},
13489
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13490
+ };
13491
+ }
13492
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13493
+ ...this.hooks.description,
13494
+ mintsId: true
13495
+ };
13496
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13497
+ listInstances = async () => {
13498
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13499
+ };
13500
+ getInstance = async ({ id }) => {
13501
+ const record = await this.handle.get(id);
13502
+ return record === null ? null : this.viewOf(record);
13503
+ };
13504
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13505
+ createInstance = async ({ id, name, kind }) => {
13506
+ const minted = id ?? this.hooks.mintId?.({
13507
+ name,
13508
+ ...kind !== void 0 ? { kind } : {}
13509
+ });
13510
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13511
+ await this.handle.put({
13512
+ id: minted,
13513
+ data: this.hooks.initialBody(name, kind)
13514
+ }, { ifRev: 0 });
13515
+ await this.hooks.onChanged?.(minted);
13516
+ return { id: minted };
13517
+ };
13518
+ updateInstance = async ({ id, name, values }) => {
13519
+ const patch = {};
13520
+ for (const [key, value] of Object.entries(values)) {
13521
+ if (value === void 0) continue;
13522
+ if (!this.secretKeys.has(key)) patch[key] = value;
13523
+ else if (isSecretClear(value)) patch[key] = null;
13524
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13525
+ }
13526
+ if (name !== void 0) patch[this.nameField] = name;
13527
+ await this.handle.patch(id, patch);
13528
+ await this.hooks.onChanged?.(id);
13529
+ return { ok: true };
13530
+ };
13531
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13532
+ setInstanceEnabled = async ({ id, enabled }) => {
13533
+ await this.handle.patch(id, { enabled });
13534
+ await this.hooks.onChanged?.(id);
13535
+ return { ok: true };
13536
+ };
13537
+ /** The row and its keychain refs go together (the records door). */
13538
+ deleteInstance = async ({ id }) => {
13539
+ await this.handle.delete(id);
13540
+ await this.hooks.onChanged?.(id);
13541
+ return { ok: true };
13542
+ };
13543
+ testInstance = async (input) => {
13544
+ if (this.hooks.test === void 0) return {
13545
+ success: false,
13546
+ status: "unsupported",
13547
+ error: "This integration declares no connection test"
13548
+ };
13549
+ return this.hooks.test(input);
13550
+ };
13551
+ /** The flat provider object to hand to `registerProvider`. */
13552
+ toProvider() {
13553
+ return {
13554
+ describe: this.describe,
13555
+ instanceFormSchema: this.instanceFormSchema,
13556
+ listInstances: this.listInstances,
13557
+ getInstance: this.getInstance,
13558
+ createInstance: this.createInstance,
13559
+ updateInstance: this.updateInstance,
13560
+ setInstanceEnabled: this.setInstanceEnabled,
13561
+ deleteInstance: this.deleteInstance,
13562
+ testInstance: this.testInstance
13563
+ };
13564
+ }
13565
+ };
13317
13566
  var EncodeProfileSchema = object({
13318
13567
  video: object({
13319
13568
  codec: _enum([
@@ -14382,6 +14631,58 @@ var MigrationOwnerStepSchema = object({
14382
14631
  /** The ledger state the engine found: `pending` has no row yet. */
14383
14632
  state: union([literal("pending"), MigrationStatusSchema])
14384
14633
  });
14634
+ function isPlain(value) {
14635
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14636
+ }
14637
+ async function flattenNestedRecords(ports, spec) {
14638
+ const ids = await ports.listNested(spec.legacyKey);
14639
+ if (ids.length === 0) return {
14640
+ outcome: "already-done",
14641
+ summary: { converted: 0 }
14642
+ };
14643
+ const rows = /* @__PURE__ */ new Map();
14644
+ for (const id of ids) try {
14645
+ rows.set(id, await ports.readResolved(id));
14646
+ } catch (err) {
14647
+ const why = err instanceof Error ? err.message : String(err);
14648
+ throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
14649
+ }
14650
+ for (const [id, row] of rows) {
14651
+ const legacy = row[spec.legacyKey];
14652
+ if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
14653
+ const flat = spec.toFlat(legacy, row);
14654
+ await ports.patch(id, flat);
14655
+ const back = await ports.readResolved(id);
14656
+ const wrong = spec.flatSecretKeys.filter((key) => {
14657
+ const sent = flat[key];
14658
+ const got = back[key];
14659
+ return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
14660
+ });
14661
+ if (wrong.length > 0) {
14662
+ await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
14663
+ throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
14664
+ }
14665
+ await ports.patch(id, { [spec.legacyKey]: null });
14666
+ }
14667
+ return {
14668
+ outcome: "done",
14669
+ summary: { converted: rows.size }
14670
+ };
14671
+ }
14672
+ /** The ports over one kind's records handle. */
14673
+ function flattenPortsOver(handle) {
14674
+ return {
14675
+ listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
14676
+ readResolved: async (id) => {
14677
+ const row = await handle.getResolved(id);
14678
+ if (row === null) throw new Error("the record is gone");
14679
+ return row.data;
14680
+ },
14681
+ patch: async (id, patch) => {
14682
+ await handle.patch(id, patch);
14683
+ }
14684
+ };
14685
+ }
14385
14686
  /**
14386
14687
  * The trainer contract — what the hub hands the cloud trainer and what it
14387
14688
  * gets back (cloud training, D805+).
@@ -49912,117 +50213,6 @@ function actuator(input) {
49912
50213
  };
49913
50214
  }
49914
50215
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
49915
- var PowerSourceRefusalReasonSchema = _enum([
49916
- "no-power-source",
49917
- "switch-missing",
49918
- "switch-offline",
49919
- "switch-unknown",
49920
- "restart-in-progress",
49921
- "switch-call-failed"
49922
- ]);
49923
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
49924
- accepted: literal(false),
49925
- reason: PowerSourceRefusalReasonSchema,
49926
- /** One line for the operator, naming the switch. */
49927
- message: string()
49928
- })]);
49929
- var PowerSourceInfoSchema = object({
49930
- /** False when the device has no power source linked; the rest is then empty. */
49931
- linked: boolean(),
49932
- switchDeviceId: number$1().nullable(),
49933
- switchName: string().nullable(),
49934
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
49935
- switchOn: boolean().nullable(),
49936
- switchOnline: boolean().nullable(),
49937
- restartInProgress: boolean(),
49938
- offSeconds: number$1().int()
49939
- });
49940
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
49941
- var PowerSourceStateSchema = object({
49942
- /** Null while the switch has not reported: unknown is never `false` (D393). */
49943
- switchOn: boolean().nullable(),
49944
- offSeconds: number$1().int(),
49945
- lastFetchedAt: number$1()
49946
- });
49947
- var POWER_SOURCE_DEVICE_TYPES = [
49948
- DeviceType.Gateway,
49949
- DeviceType.Hub,
49950
- DeviceType.Camera,
49951
- DeviceType.Sensor,
49952
- DeviceType.MediaPlayer,
49953
- DeviceType.Vacuum,
49954
- DeviceType.Generic,
49955
- DeviceType.Washer,
49956
- DeviceType.Dryer,
49957
- DeviceType.Dishwasher,
49958
- DeviceType.PetFountain,
49959
- DeviceType.Fan,
49960
- DeviceType.Humidifier,
49961
- DeviceType.Climate,
49962
- DeviceType.WaterHeater
49963
- ];
49964
- var powerSourceCustomize = {
49965
- version: 1,
49966
- targets: [{
49967
- id: "switch",
49968
- cardinality: "scalar",
49969
- vocabulary: {
49970
- cap: "power-source",
49971
- stateField: "switchOn"
49972
- },
49973
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49974
- apply: "add",
49975
- path: "switchOn",
49976
- sources: [{
49977
- kind: "switch",
49978
- transforms: [{ kind: "identity" }]
49979
- }],
49980
- companions: [{
49981
- kind: "constant",
49982
- path: "offSeconds",
49983
- value: 10,
49984
- when: "add"
49985
- }, {
49986
- kind: "clock",
49987
- path: "lastFetchedAt",
49988
- when: "always"
49989
- }],
49990
- sourceReport: { status: "assignment-status" }
49991
- }]
49992
- };
49993
- var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
49994
- var powerSourceCapability = {
49995
- name: "power-source",
49996
- scope: "device",
49997
- mode: "singleton",
49998
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49999
- methods: {
50000
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
50001
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50002
- kind: "mutation",
50003
- auth: "admin"
50004
- }),
50005
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50006
- kind: "mutation",
50007
- auth: "admin"
50008
- }),
50009
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50010
- kind: "mutation",
50011
- auth: "admin"
50012
- })
50013
- },
50014
- runtimeState: PowerSourceStateSchema,
50015
- customize: powerSourceCustomize,
50016
- /**
50017
- * Runtime-state durability: **session** — mirrored from the linked switch on
50018
- * its next report; a restored value would outlive the reading it names.
50019
- *
50020
- * See `RuntimeStateDurability`. Enforced by
50021
- * `scripts/check-runtime-state-durability.ts`.
50022
- */
50023
- durability: "session",
50024
- history: "none"
50025
- };
50026
50216
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
50027
50217
  DeviceType.Camera;
50028
50218
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -50050,6 +50240,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
50050
50240
  }).optional()
50051
50241
  })) });
50052
50242
  /**
50243
+ * A saved Customize assignment (D936). The schema is the type: callers use
50244
+ * `z.infer`, they do not restate the shape.
50245
+ */
50246
+ var OverrideSourceSchema = object({
50247
+ addonId: string().min(1),
50248
+ stableId: string().min(1),
50249
+ cap: string().min(1),
50250
+ fieldPath: string().min(1)
50251
+ });
50252
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50253
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50254
+ var ThresholdTransformDraftSchema = object({
50255
+ kind: literal("threshold"),
50256
+ comparison: _enum(["gte", "lte"]),
50257
+ threshold: number$1(),
50258
+ startDwellMs: number$1().nonnegative(),
50259
+ stopDwellMs: number$1().nonnegative()
50260
+ });
50261
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50262
+ IdentityTransformDraftSchema,
50263
+ InvertTransformDraftSchema,
50264
+ ThresholdTransformDraftSchema,
50265
+ object({
50266
+ kind: literal("linear"),
50267
+ scale: number$1(),
50268
+ offset: number$1(),
50269
+ clampMin: number$1().optional(),
50270
+ clampMax: number$1().optional()
50271
+ }),
50272
+ object({
50273
+ kind: literal("enum-map"),
50274
+ mapping: record(string(), string().nullable())
50275
+ }),
50276
+ object({ kind: literal("number") })
50277
+ ]);
50278
+ var ScalarOverrideDraftSchema = object({
50279
+ kind: literal("scalar"),
50280
+ cap: string().min(1),
50281
+ targetId: string().min(1),
50282
+ source: OverrideSourceSchema,
50283
+ transform: CustomizeTransformDraftSchema
50284
+ });
50285
+ var KeyedArrayOverrideDraftSchema = object({
50286
+ kind: literal("keyedArray"),
50287
+ cap: string().min(1),
50288
+ targetId: string().min(1),
50289
+ key: string().min(1),
50290
+ label: string().min(1),
50291
+ fieldPath: string().min(1),
50292
+ source: OverrideSourceSchema,
50293
+ transform: CustomizeTransformDraftSchema
50294
+ });
50295
+ var PulseOverrideDraftSchema = object({
50296
+ kind: literal("pulse"),
50297
+ cap: string().min(1),
50298
+ targetId: string().min(1),
50299
+ enabled: boolean(),
50300
+ debounceMs: number$1().nonnegative(),
50301
+ edges: array(_enum([
50302
+ "rising",
50303
+ "button",
50304
+ "native-event"
50305
+ ])).min(1).optional(),
50306
+ sources: array(OverrideSourceSchema).min(1)
50307
+ });
50308
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50309
+ "in-addition",
50310
+ "confirm",
50311
+ "replace"
50312
+ ]).optional() });
50313
+ discriminatedUnion("kind", [
50314
+ ScalarOverrideDraftSchema,
50315
+ KeyedArrayOverrideDraftSchema,
50316
+ PulseOverrideDraftSchema,
50317
+ object({
50318
+ kind: literal("span"),
50319
+ cap: string().min(1),
50320
+ targetId: string().min(1),
50321
+ timeoutMs: number$1().nonnegative(),
50322
+ cooldownMs: number$1().nonnegative().optional(),
50323
+ sources: array(SpanSourceDraftSchema)
50324
+ })
50325
+ ]);
50326
+ object({
50327
+ targetId: string().min(1),
50328
+ configured: boolean(),
50329
+ source: _enum(["native", "assigned"]),
50330
+ state: _enum([
50331
+ "active",
50332
+ "stale",
50333
+ "released",
50334
+ "unavailable"
50335
+ ]),
50336
+ reason: string().nullable()
50337
+ });
50338
+ /**
50339
+ * What a running-threshold proposal says (planning#405). The schema is the
50340
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50341
+ *
50342
+ * A proposal is read-only advice: it carries the numbers it was derived from
50343
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50344
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50345
+ */
50346
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50347
+ var RunningProposalConfidenceSchema = _enum([
50348
+ "high",
50349
+ "medium",
50350
+ "low"
50351
+ ]);
50352
+ var RunningProposalInsufficientReasonSchema = _enum([
50353
+ "no-history",
50354
+ "span-too-short",
50355
+ "truncated",
50356
+ "no-idle-floor",
50357
+ "no-runs",
50358
+ "no-separation"
50359
+ ]);
50360
+ var RunningProposalReplaySchema = object({
50361
+ /** Intervals the real dwell expression held true over the window. */
50362
+ intervals: number$1().int().nonnegative(),
50363
+ /** Intervals outside every run, or a second interval inside one. */
50364
+ extra: number$1().int().nonnegative(),
50365
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50366
+ missed: number$1().int().nonnegative()
50367
+ });
50368
+ var RunningProposalEvidenceSchema = object({
50369
+ tier: RunningProposalTierSchema,
50370
+ spanMs: number$1().nonnegative(),
50371
+ from: number$1(),
50372
+ to: number$1(),
50373
+ points: number$1().int().nonnegative(),
50374
+ runsFound: number$1().int().nonnegative(),
50375
+ pausesFound: number$1().int().nonnegative(),
50376
+ longestPauseMs: number$1().nonnegative(),
50377
+ idleFloorW: number$1(),
50378
+ noiseCeilingW: number$1(),
50379
+ plateauW: number$1(),
50380
+ replay: RunningProposalReplaySchema
50381
+ });
50382
+ var RunningProposalInsufficientEvidenceSchema = object({
50383
+ spanMs: number$1().nonnegative(),
50384
+ points: number$1().int().nonnegative(),
50385
+ runsFound: number$1().int().nonnegative()
50386
+ });
50387
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50388
+ state: literal("proposal"),
50389
+ transform: ThresholdTransformDraftSchema,
50390
+ evidence: RunningProposalEvidenceSchema,
50391
+ confidence: RunningProposalConfidenceSchema,
50392
+ /** Why the confidence is not higher; empty when nothing held it back. */
50393
+ notes: array(string())
50394
+ }), object({
50395
+ state: literal("insufficient"),
50396
+ reason: RunningProposalInsufficientReasonSchema,
50397
+ evidence: RunningProposalInsufficientEvidenceSchema,
50398
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50399
+ fallback: ThresholdTransformDraftSchema
50400
+ })]);
50401
+ /**
50053
50402
  * The report of a one-shot "move this owner's plaintext secrets into the
50054
50403
  * keychain" run (D878). COUNTS only - never a value, never a ref.
50055
50404
  *
@@ -50339,6 +50688,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
50339
50688
  kind: "mutation",
50340
50689
  auth: "admin",
50341
50690
  access: "view"
50691
+ }), method(object({
50692
+ deviceId: number$1().int(),
50693
+ cap: string().min(1),
50694
+ fieldPath: string().min(1),
50695
+ deviceType: _enum(DeviceType).optional()
50696
+ }), RunningThresholdProposalSchema, {
50697
+ auth: "admin",
50698
+ access: "view"
50342
50699
  }), method(object({
50343
50700
  blockId: string().min(1),
50344
50701
  trace: AutomationRunTraceSchema.optional(),
@@ -51294,7 +51651,27 @@ method(object({
51294
51651
  * exposes only the COMMON surface: link state, supported device
51295
51652
  * kinds (so the UI can filter the device picker), and per-device
51296
51653
  * expose/unexpose.
51297
- */
51654
+ *
51655
+ * INSTANCES. One export addon can serve several independent targets (a
51656
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
51657
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
51658
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
51659
+ * default instance's); present = that instance; an id the provider does not
51660
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
51661
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
51662
+ * change always names the target it changes. A single-instance provider
51663
+ * accepts an absent id on a read or its own id, and refuses any other.
51664
+ */
51665
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
51666
+ var InstanceIdSchema$1 = string().min(1);
51667
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
51668
+ /** One independent export target of an addon. */
51669
+ var ExportInstanceSchema = object({
51670
+ instanceId: InstanceIdSchema$1,
51671
+ name: string(),
51672
+ enabled: boolean(),
51673
+ kind: string().optional()
51674
+ });
51298
51675
  var LinkStateSchema = _enum([
51299
51676
  "unlinked",
51300
51677
  "linked",
@@ -51345,10 +51722,14 @@ var ExposedDeviceSchema = object({
51345
51722
  capabilities: array(string()).optional()
51346
51723
  });
51347
51724
  var ExposeInputSchema = object({
51725
+ instanceId: InstanceIdSchema$1,
51348
51726
  deviceId: string(),
51349
51727
  capabilities: array(string()).optional()
51350
51728
  });
51351
- var UnexposeInputSchema = object({ deviceId: string() });
51729
+ var UnexposeInputSchema = object({
51730
+ instanceId: InstanceIdSchema$1,
51731
+ deviceId: string()
51732
+ });
51352
51733
  /**
51353
51734
  * What an exporter may expose besides devices (D968). An action and a
51354
51735
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -51377,15 +51758,21 @@ var ExposedEntitySchema = object({
51377
51758
  entity: ExportEntityRefSchema,
51378
51759
  exposedAs: string().optional()
51379
51760
  });
51380
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
51381
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
51382
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51761
+ var ExposeEntityInputSchema = object({
51762
+ instanceId: InstanceIdSchema$1,
51763
+ entity: ExportEntityRefSchema
51764
+ });
51765
+ var UnexposeEntityInputSchema = object({
51766
+ instanceId: InstanceIdSchema$1,
51767
+ entity: ExportEntityRefSchema
51768
+ });
51769
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51383
51770
  kind: "mutation",
51384
51771
  auth: "admin"
51385
51772
  }), method(UnexposeInputSchema, _void(), {
51386
51773
  kind: "mutation",
51387
51774
  auth: "admin"
51388
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51775
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51389
51776
  kind: "mutation",
51390
51777
  auth: "admin"
51391
51778
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -54976,15 +55363,15 @@ var FanPresetCaptureResultSchema = object({
54976
55363
  items: array(FanCaptureItemSchema),
54977
55364
  omitted: array(FanCaptureOmissionSchema)
54978
55365
  });
54979
- var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
55366
+ var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
54980
55367
  DeviceType.Fan;
54981
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55368
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
54982
55369
  kind: "mutation",
54983
55370
  auth: "admin"
54984
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55371
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
54985
55372
  kind: "mutation",
54986
55373
  auth: "protected"
54987
- }), method(deviceInput$1.extend({
55374
+ }), method(deviceInput$2.extend({
54988
55375
  fields: array(FanCaptureFieldSchema).optional(),
54989
55376
  steps: array(ActionStepSchema).max(20).default([])
54990
55377
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -55895,13 +56282,50 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55895
56282
  */
55896
56283
  var ConfigSchemaPassthrough$3 = custom();
55897
56284
  var IntegrationInstanceIdSchema = string().min(1);
56285
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56286
+ /**
56287
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56288
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56289
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56290
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56291
+ * instance id.
56292
+ */
56293
+ var IntegrationInstanceKindSchema = object({
56294
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56295
+ id: string().min(1),
56296
+ label: string().min(1),
56297
+ /** The kind signs in through a server-held interactive session. */
56298
+ signIn: object({
56299
+ basePath: string().min(1),
56300
+ methods: array(_enum(["device-code", "proxy"])).optional()
56301
+ }).optional(),
56302
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56303
+ connectPath: string().min(1).optional(),
56304
+ /** Relative hub path to disconnect that account. */
56305
+ disconnectPath: string().min(1).optional()
56306
+ });
55898
56307
  var IntegrationInstanceDescriptionSchema = object({
55899
56308
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55900
- instanceMode: _enum(["single", "multiple"]),
56309
+ instanceMode: IntegrationInstanceModeSchema,
55901
56310
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55902
56311
  kind: string(),
55903
56312
  /** True when `testInstance` really probes the remote. */
55904
- canTest: boolean()
56313
+ canTest: boolean(),
56314
+ /**
56315
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56316
+ * Add flow asks which first and sends it as `createInstance.kind`.
56317
+ */
56318
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56319
+ /**
56320
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56321
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56322
+ */
56323
+ allowRestoreId: boolean().optional(),
56324
+ /**
56325
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56326
+ * send an id (the UI then mints an opaque one).
56327
+ */
56328
+ mintsId: boolean().optional()
55905
56329
  });
55906
56330
  var IntegrationInstanceStatusSchema = object({
55907
56331
  state: _enum([
@@ -55910,7 +56334,9 @@ var IntegrationInstanceStatusSchema = object({
55910
56334
  "error",
55911
56335
  "unknown"
55912
56336
  ]),
55913
- detail: string().optional()
56337
+ detail: string().optional(),
56338
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56339
+ label: string().optional()
55914
56340
  });
55915
56341
  var IntegrationInstanceSchema = object({
55916
56342
  id: IntegrationInstanceIdSchema,
@@ -55922,9 +56348,13 @@ var IntegrationInstanceSchema = object({
55922
56348
  * (`enabled !== false`, so a row written before the field existed is on).
55923
56349
  */
55924
56350
  enabled: boolean(),
56351
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56352
+ kind: string().optional(),
55925
56353
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55926
56354
  formValues: record(string(), unknown()),
55927
- status: IntegrationInstanceStatusSchema.optional()
56355
+ status: IntegrationInstanceStatusSchema.optional(),
56356
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56357
+ lastCheckedAt: number$1().optional()
55928
56358
  });
55929
56359
  var IntegrationInstancePatchSchema = object({
55930
56360
  id: IntegrationInstanceIdSchema,
@@ -55932,31 +56362,57 @@ var IntegrationInstancePatchSchema = object({
55932
56362
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55933
56363
  values: record(string(), unknown())
55934
56364
  });
55935
- 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({
55936
- id: IntegrationInstanceIdSchema,
55937
- name: string().min(1)
55938
- }), object({ id: string() }), {
55939
- kind: "mutation",
55940
- auth: "admin"
55941
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55942
- kind: "mutation",
55943
- auth: "admin"
55944
- }), method(object({
55945
- id: IntegrationInstanceIdSchema,
55946
- enabled: boolean()
55947
- }), object({ ok: literal(true) }), {
55948
- kind: "mutation",
55949
- auth: "admin"
55950
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55951
- kind: "mutation",
55952
- auth: "admin"
55953
- }), optionalMethod(object({
55954
- id: IntegrationInstanceIdSchema.nullable(),
55955
- values: record(string(), unknown())
55956
- }), TestConnectionResultSchema$1, {
55957
- kind: "mutation",
55958
- auth: "admin"
55959
- });
56365
+ var integrationInstancesCapability = {
56366
+ name: "integration-instances",
56367
+ scope: "system",
56368
+ mode: "collection",
56369
+ methods: {
56370
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56371
+ /** The form; `id: null` is the create form. */
56372
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56373
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56374
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56375
+ /**
56376
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56377
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56378
+ */
56379
+ createInstance: method(object({
56380
+ id: IntegrationInstanceIdSchema.optional(),
56381
+ name: string().min(1),
56382
+ kind: string().min(1).optional()
56383
+ }), object({ id: string() }), {
56384
+ kind: "mutation",
56385
+ auth: "admin"
56386
+ }),
56387
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56388
+ kind: "mutation",
56389
+ auth: "admin"
56390
+ }),
56391
+ /**
56392
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56393
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56394
+ */
56395
+ setInstanceEnabled: method(object({
56396
+ id: IntegrationInstanceIdSchema,
56397
+ enabled: boolean()
56398
+ }), object({ ok: literal(true) }), {
56399
+ kind: "mutation",
56400
+ auth: "admin"
56401
+ }),
56402
+ /** Delete the record and its keychain refs together. */
56403
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56404
+ kind: "mutation",
56405
+ auth: "admin"
56406
+ }),
56407
+ testInstance: optionalMethod(object({
56408
+ id: IntegrationInstanceIdSchema.nullable(),
56409
+ values: record(string(), unknown())
56410
+ }), TestConnectionResultSchema$1, {
56411
+ kind: "mutation",
56412
+ auth: "admin"
56413
+ })
56414
+ }
56415
+ };
55960
56416
  /**
55961
56417
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55962
56418
  * session (server-initiated offer pattern, identical to the main
@@ -57974,6 +58430,11 @@ var LogEntrySchema = object({
57974
58430
  * flight, and not asking for the next slice ends the scan.
57975
58431
  */
57976
58432
  var LogReadQuerySchema = object({
58433
+ /**
58434
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58435
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58436
+ */
58437
+ instanceId: string().min(1).max(200).optional(),
57977
58438
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
57978
58439
  requestId: string().min(1).max(64).optional(),
57979
58440
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -58036,6 +58497,8 @@ var LogReadResultSchema = object({
58036
58497
  /** True when `cancelRead` ended this slice early. */
58037
58498
  cancelled: boolean().optional()
58038
58499
  });
58500
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58501
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
58039
58502
  var LogReadCapabilitiesSchema = object({
58040
58503
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
58041
58504
  readable: boolean(),
@@ -58054,7 +58517,7 @@ var LogReadCapabilitiesSchema = object({
58054
58517
  /** How far back the store reaches, when the provider knows (days). */
58055
58518
  retentionDays: number$1().nullable()
58056
58519
  });
58057
- 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() }), {
58520
+ 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() }), {
58058
58521
  kind: "mutation",
58059
58522
  auth: "admin"
58060
58523
  });
@@ -70448,6 +70911,117 @@ var powerMeterCapability = {
70448
70911
  volatileStateFields: ["lastFetchedAt"],
70449
70912
  customize: powerMeterCustomize
70450
70913
  };
70914
+ var PowerSourceRefusalReasonSchema = _enum([
70915
+ "no-power-source",
70916
+ "switch-missing",
70917
+ "switch-offline",
70918
+ "switch-unknown",
70919
+ "restart-in-progress",
70920
+ "switch-call-failed"
70921
+ ]);
70922
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
70923
+ accepted: literal(false),
70924
+ reason: PowerSourceRefusalReasonSchema,
70925
+ /** One line for the operator, naming the switch. */
70926
+ message: string()
70927
+ })]);
70928
+ var PowerSourceInfoSchema = object({
70929
+ /** False when the device has no power source linked; the rest is then empty. */
70930
+ linked: boolean(),
70931
+ switchDeviceId: number$1().nullable(),
70932
+ switchName: string().nullable(),
70933
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
70934
+ switchOn: boolean().nullable(),
70935
+ switchOnline: boolean().nullable(),
70936
+ restartInProgress: boolean(),
70937
+ offSeconds: number$1().int()
70938
+ });
70939
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
70940
+ var PowerSourceStateSchema = object({
70941
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
70942
+ switchOn: boolean().nullable(),
70943
+ offSeconds: number$1().int(),
70944
+ lastFetchedAt: number$1()
70945
+ });
70946
+ var POWER_SOURCE_DEVICE_TYPES = [
70947
+ DeviceType.Gateway,
70948
+ DeviceType.Hub,
70949
+ DeviceType.Camera,
70950
+ DeviceType.Sensor,
70951
+ DeviceType.MediaPlayer,
70952
+ DeviceType.Vacuum,
70953
+ DeviceType.Generic,
70954
+ DeviceType.Washer,
70955
+ DeviceType.Dryer,
70956
+ DeviceType.Dishwasher,
70957
+ DeviceType.PetFountain,
70958
+ DeviceType.Fan,
70959
+ DeviceType.Humidifier,
70960
+ DeviceType.Climate,
70961
+ DeviceType.WaterHeater
70962
+ ];
70963
+ var powerSourceCustomize = {
70964
+ version: 1,
70965
+ targets: [{
70966
+ id: "switch",
70967
+ cardinality: "scalar",
70968
+ vocabulary: {
70969
+ cap: "power-source",
70970
+ stateField: "switchOn"
70971
+ },
70972
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70973
+ apply: "add",
70974
+ path: "switchOn",
70975
+ sources: [{
70976
+ kind: "switch",
70977
+ transforms: [{ kind: "identity" }]
70978
+ }],
70979
+ companions: [{
70980
+ kind: "constant",
70981
+ path: "offSeconds",
70982
+ value: 10,
70983
+ when: "add"
70984
+ }, {
70985
+ kind: "clock",
70986
+ path: "lastFetchedAt",
70987
+ when: "always"
70988
+ }],
70989
+ sourceReport: { status: "assignment-status" }
70990
+ }]
70991
+ };
70992
+ var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
70993
+ var powerSourceCapability = {
70994
+ name: "power-source",
70995
+ scope: "device",
70996
+ mode: "singleton",
70997
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70998
+ methods: {
70999
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
71000
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71001
+ kind: "mutation",
71002
+ auth: "admin"
71003
+ }),
71004
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71005
+ kind: "mutation",
71006
+ auth: "admin"
71007
+ }),
71008
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71009
+ kind: "mutation",
71010
+ auth: "admin"
71011
+ })
71012
+ },
71013
+ runtimeState: PowerSourceStateSchema,
71014
+ customize: powerSourceCustomize,
71015
+ /**
71016
+ * Runtime-state durability: **session** — mirrored from the linked switch on
71017
+ * its next report; a restored value would outlive the reading it names.
71018
+ *
71019
+ * See `RuntimeStateDurability`. Enforced by
71020
+ * `scripts/check-runtime-state-durability.ts`.
71021
+ */
71022
+ durability: "session",
71023
+ history: "none"
71024
+ };
70451
71025
  /**
70452
71026
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
70453
71027
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -79809,6 +80383,25 @@ function buildDescriptor(capName, kind) {
79809
80383
  * contributes one entry per bound cap present in this map.
79810
80384
  */
79811
80385
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
80386
+ /**
80387
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
80388
+ * shared by the admin UI and the viewer. A role is data on the wire; each
80389
+ * client resolves its name from `label` (English and Italian side by side) and
80390
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
80391
+ * server sends an id, a client maps it to its own icon set.
80392
+ *
80393
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
80394
+ * pictogram): a client that can draw text shows it, one that cannot falls back
80395
+ * to `icon`.
80396
+ */
80397
+ function digit(n) {
80398
+ return {
80399
+ label: t(n, n),
80400
+ icon: "hash",
80401
+ legend: n
80402
+ };
80403
+ }
80404
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
79812
80405
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
79813
80406
  var btn = (idSuffix, row, col, init) => ({
79814
80407
  idSuffix,
@@ -79936,6 +80529,34 @@ discriminatedUnion("kind", [object({
79936
80529
  kind: literal("pronto"),
79937
80530
  code: string()
79938
80531
  })]);
80532
+ /**
80533
+ * Propose a running threshold from a device's own power history (planning#405).
80534
+ *
80535
+ * Pure and deterministic: the same series gives the same answer. The series is
80536
+ * a HELD-VALUE step series — a point's value stands until the next point — which
80537
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
80538
+ * count of samples.
80539
+ *
80540
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80541
+ * near F. A device that is not near one level most of the time has no floor.
80542
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80543
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80544
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80545
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80546
+ * grid a person would type; stop dwell covers the longest pause; start dwell
80547
+ * covers the longest standby blip above T.
80548
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80549
+ * run must hold exactly one true interval and nothing may flap outside.
80550
+ */
80551
+ var SECOND_MS = 1e3;
80552
+ var MINUTE_MS$1 = 60 * SECOND_MS;
80553
+ 24 * (60 * MINUTE_MS$1);
80554
+ 10 * MINUTE_MS$1;
80555
+ 30 * SECOND_MS;
80556
+ 60 * SECOND_MS;
80557
+ 20 * MINUTE_MS$1;
80558
+ 10 * SECOND_MS;
80559
+ 10 * SECOND_MS;
79939
80560
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79940
80561
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79941
80562
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -83041,6 +83662,13 @@ Object.freeze({
83041
83662
  access: "create",
83042
83663
  role: "admin"
83043
83664
  },
83665
+ "coreBlocks.proposeRunningThreshold": {
83666
+ capName: "core-blocks",
83667
+ capScope: "system",
83668
+ addonId: null,
83669
+ access: "view",
83670
+ role: "admin"
83671
+ },
83044
83672
  "coreBlocks.reportAutomationRun": {
83045
83673
  capName: "core-blocks",
83046
83674
  capScope: "system",
@@ -83552,6 +84180,13 @@ Object.freeze({
83552
84180
  access: "view",
83553
84181
  role: "admin"
83554
84182
  },
84183
+ "deviceExport.listInstances": {
84184
+ capName: "device-export",
84185
+ capScope: "system",
84186
+ addonId: null,
84187
+ access: "view",
84188
+ role: "admin"
84189
+ },
83555
84190
  "deviceExport.listSupportedDeviceKinds": {
83556
84191
  capName: "device-export",
83557
84192
  capScope: "system",
@@ -92499,6 +93134,11 @@ Object.freeze({
92499
93134
  form: "single",
92500
93135
  optional: false
92501
93136
  }],
93137
+ "coreBlocks.proposeRunningThreshold": [{
93138
+ name: "deviceId",
93139
+ form: "single",
93140
+ optional: false
93141
+ }],
92502
93142
  "cover.close": [{
92503
93143
  name: "deviceId",
92504
93144
  form: "single",
@@ -95162,99 +95802,6 @@ Object.freeze({
95162
95802
  "network-access": "ingress",
95163
95803
  "smtp-provider": "email"
95164
95804
  });
95165
- /**
95166
- * A saved Customize assignment (D936). The schema is the type: callers use
95167
- * `z.infer`, they do not restate the shape.
95168
- */
95169
- var OverrideSourceSchema = object({
95170
- addonId: string().min(1),
95171
- stableId: string().min(1),
95172
- cap: string().min(1),
95173
- fieldPath: string().min(1)
95174
- });
95175
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
95176
- object({ kind: literal("identity") }),
95177
- object({ kind: literal("invert") }),
95178
- object({
95179
- kind: literal("threshold"),
95180
- comparison: _enum(["gte", "lte"]),
95181
- threshold: number$1(),
95182
- startDwellMs: number$1().nonnegative(),
95183
- stopDwellMs: number$1().nonnegative()
95184
- }),
95185
- object({
95186
- kind: literal("linear"),
95187
- scale: number$1(),
95188
- offset: number$1(),
95189
- clampMin: number$1().optional(),
95190
- clampMax: number$1().optional()
95191
- }),
95192
- object({
95193
- kind: literal("enum-map"),
95194
- mapping: record(string(), string().nullable())
95195
- }),
95196
- object({ kind: literal("number") })
95197
- ]);
95198
- var ScalarOverrideDraftSchema = object({
95199
- kind: literal("scalar"),
95200
- cap: string().min(1),
95201
- targetId: string().min(1),
95202
- source: OverrideSourceSchema,
95203
- transform: CustomizeTransformDraftSchema
95204
- });
95205
- var KeyedArrayOverrideDraftSchema = object({
95206
- kind: literal("keyedArray"),
95207
- cap: string().min(1),
95208
- targetId: string().min(1),
95209
- key: string().min(1),
95210
- label: string().min(1),
95211
- fieldPath: string().min(1),
95212
- source: OverrideSourceSchema,
95213
- transform: CustomizeTransformDraftSchema
95214
- });
95215
- var PulseOverrideDraftSchema = object({
95216
- kind: literal("pulse"),
95217
- cap: string().min(1),
95218
- targetId: string().min(1),
95219
- enabled: boolean(),
95220
- debounceMs: number$1().nonnegative(),
95221
- edges: array(_enum([
95222
- "rising",
95223
- "button",
95224
- "native-event"
95225
- ])).min(1).optional(),
95226
- sources: array(OverrideSourceSchema).min(1)
95227
- });
95228
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
95229
- "in-addition",
95230
- "confirm",
95231
- "replace"
95232
- ]).optional() });
95233
- discriminatedUnion("kind", [
95234
- ScalarOverrideDraftSchema,
95235
- KeyedArrayOverrideDraftSchema,
95236
- PulseOverrideDraftSchema,
95237
- object({
95238
- kind: literal("span"),
95239
- cap: string().min(1),
95240
- targetId: string().min(1),
95241
- timeoutMs: number$1().nonnegative(),
95242
- cooldownMs: number$1().nonnegative().optional(),
95243
- sources: array(SpanSourceDraftSchema)
95244
- })
95245
- ]);
95246
- object({
95247
- targetId: string().min(1),
95248
- configured: boolean(),
95249
- source: _enum(["native", "assigned"]),
95250
- state: _enum([
95251
- "active",
95252
- "stale",
95253
- "released",
95254
- "unavailable"
95255
- ]),
95256
- reason: string().nullable()
95257
- });
95258
95805
  var G711_SCALE_CORRECTION_DB = {
95259
95806
  PCMU: 20 * Math.log10(4),
95260
95807
  PCMA: 20 * Math.log10(8)
@@ -97560,7 +98107,7 @@ var alexaConnectionSchema = object({
97560
98107
  */
97561
98108
  refreshToken: string().default("").describe("Alexa refresh token (must be inside cookie blob)").meta({ secret: true }),
97562
98109
  /** Device-registration secret some flows need. */
97563
- macDms: string().default("").describe("macDms device-registration secret (optional)"),
98110
+ macDms: string().default("").describe("macDms device-registration secret (optional)").meta({ secret: true }),
97564
98111
  /** Poll interval for state sync (Alexa has no clean 3rd-party push). */
97565
98112
  pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number$1().int().min(15e3).max(36e5).default(6e4)).describe("State poll interval (ms) — conservative to avoid Amazon throttling")
97566
98113
  });
@@ -97578,7 +98125,9 @@ var AlexaBrokerEntrySchema = object({
97578
98125
  id: string().min(1),
97579
98126
  name: string().min(1),
97580
98127
  connection: alexaConnectionSchema,
97581
- integrationId: string().optional()
98128
+ integrationId: string().optional(),
98129
+ /** Absent means on; the instances page switches a connection off without deleting it. */
98130
+ enabled: boolean().optional()
97582
98131
  });
97583
98132
  /**
97584
98133
  * Interactive proxy-login settings (addon-global, not per-broker). alexa-remote2
@@ -97615,7 +98164,6 @@ function readEchoTargets(config) {
97615
98164
  }
97616
98165
  /** Top-level addon config — broker entries + the proxy-login settings. */
97617
98166
  var alexaAddonConfigSchema = object({
97618
- brokers: array(AlexaBrokerEntrySchema).default([]),
97619
98167
  proxyLogin: alexaProxyLoginSchema.default({ proxyPort: 3456 }),
97620
98168
  echoTargets: array(alexaEchoTargetSchema).default([])
97621
98169
  });
@@ -141160,13 +141708,16 @@ var AlexaBrokerRegistry = class {
141160
141708
  }
141161
141709
  async restore(entries) {
141162
141710
  for (const entry of entries) this.#seedCounter(entry.id);
141163
- for (const entry of entries) try {
141164
- await this.#startManager(entry);
141165
- } catch (err) {
141166
- this.#logger.warn("AlexaBrokerRegistry: failed to restore manager", {
141167
- tags: { brokerId: entry.id },
141168
- meta: { error: errMsg(err) }
141169
- });
141711
+ for (const entry of entries) {
141712
+ if (entry.enabled === false) continue;
141713
+ try {
141714
+ await this.#startManager(entry);
141715
+ } catch (err) {
141716
+ this.#logger.warn("AlexaBrokerRegistry: failed to restore manager", {
141717
+ tags: { brokerId: entry.id },
141718
+ meta: { error: errMsg(err) }
141719
+ });
141720
+ }
141170
141721
  }
141171
141722
  }
141172
141723
  async shutdown() {
@@ -141200,7 +141751,8 @@ var AlexaBrokerRegistry = class {
141200
141751
  this.#onStatesPolled = cb;
141201
141752
  }
141202
141753
  async createEntry(name, connection, opts = {}) {
141203
- const id = this.#allocateId();
141754
+ const id = opts.id ?? this.#allocateId();
141755
+ if (opts.id !== void 0) this.#seedCounter(opts.id);
141204
141756
  const entry = {
141205
141757
  id,
141206
141758
  name,
@@ -141213,7 +141765,7 @@ var AlexaBrokerRegistry = class {
141213
141765
  }
141214
141766
  async removeEntry(id) {
141215
141767
  const mgr = this.#managers.get(id);
141216
- if (!mgr) throw new Error(`AlexaBrokerRegistry: unknown broker id "${id}"`);
141768
+ if (!mgr) return;
141217
141769
  for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
141218
141770
  this.#managers.delete(id);
141219
141771
  try {
@@ -141280,6 +141832,10 @@ var AlexaBrokerRegistry = class {
141280
141832
  if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
141281
141833
  await mgr.start();
141282
141834
  }
141835
+ /** The next free broker id, for a record minted before its manager exists. */
141836
+ allocateId() {
141837
+ return this.#allocateId();
141838
+ }
141283
141839
  #allocateId() {
141284
141840
  const id = `alexa_${String(this.#nextId).padStart(3, "0")}`;
141285
141841
  this.#nextId++;
@@ -142701,10 +143257,12 @@ var AlexaEchoDevice = class extends BaseDevice {
142701
143257
  var alexaBrokersEntryKind = defineEntryKind("brokers", {
142702
143258
  title: "Alexa import brokers",
142703
143259
  schema: entryBody({
142704
- connection: AlexaBrokerEntrySchema.shape.connection,
142705
- integrationId: string().optional()
143260
+ ...alexaConnectionSchema.shape,
143261
+ integrationId: string().optional(),
143262
+ enabled: boolean().optional()
142706
143263
  }),
142707
- writeTrigger: "an operator links or removes an Alexa account broker"
143264
+ writeTrigger: "an operator links or removes an Alexa account broker",
143265
+ retiredSecretPaths: ["connection.cookie", "connection.refreshToken"]
142708
143266
  });
142709
143267
  var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
142710
143268
  title: "Alexa Echo speech sessions",
@@ -142717,13 +143275,118 @@ var alexaRecordKinds = {
142717
143275
  "speak-sessions": alexaSpeakSessionsEntryKind
142718
143276
  };
142719
143277
  //#endregion
143278
+ //#region src/alexa-instances.ts
143279
+ /**
143280
+ * The Alexa accounts as `integration-instances` (planning#408 P3): one instance per `brokers`
143281
+ * record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves. The body is flat, so
143282
+ * the form the instances page edits is the stored shape. Signing in (device code or the Amazon
143283
+ * login proxy) goes through the routes the addon already serves under `/addon/import-alexa/login`.
143284
+ */
143285
+ var ALEXA_INSTANCE_KIND = "alexa";
143286
+ var ALEXA_SIGN_IN_PATH = "/addon/import-alexa/login";
143287
+ function isAlexaBodyConfigured(body) {
143288
+ return body.cookie.length > 0 || body.refreshToken.length > 0;
143289
+ }
143290
+ function statusOf(info, configured) {
143291
+ if (!configured) return {
143292
+ state: "unknown",
143293
+ label: "Not signed in"
143294
+ };
143295
+ if (info === null) return {
143296
+ state: "unknown",
143297
+ label: "Not connected"
143298
+ };
143299
+ if (info.status === "connected") return {
143300
+ state: "ok",
143301
+ label: "Connected"
143302
+ };
143303
+ if (info.status === "connecting") return {
143304
+ state: "unknown",
143305
+ label: "Connecting"
143306
+ };
143307
+ return {
143308
+ state: "error",
143309
+ label: info.status,
143310
+ ...info.error !== null ? { detail: info.error } : {}
143311
+ };
143312
+ }
143313
+ function buildAlexaInstances(records, deps) {
143314
+ return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
143315
+ description: {
143316
+ instanceMode: "multiple",
143317
+ kind: "account",
143318
+ canTest: true,
143319
+ kinds: [{
143320
+ id: ALEXA_INSTANCE_KIND,
143321
+ label: "Alexa",
143322
+ signIn: {
143323
+ basePath: ALEXA_SIGN_IN_PATH,
143324
+ methods: ["device-code", "proxy"]
143325
+ }
143326
+ }],
143327
+ allowRestoreId: true
143328
+ },
143329
+ formSchema: () => buildConnectionFormSchema(),
143330
+ initialBody: (name) => ({
143331
+ name,
143332
+ amazonDomain: "amazon.com",
143333
+ cookie: "",
143334
+ refreshToken: "",
143335
+ macDms: "",
143336
+ pollIntervalMs: 6e4
143337
+ }),
143338
+ mintId: () => deps.mintId(),
143339
+ kindOf: () => ALEXA_INSTANCE_KIND,
143340
+ health: (body, id) => {
143341
+ const info = deps.infoOf(id);
143342
+ return {
143343
+ status: statusOf(info, isAlexaBodyConfigured(body)),
143344
+ ...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
143345
+ };
143346
+ },
143347
+ configured: isAlexaBodyConfigured,
143348
+ onChanged: deps.onChanged,
143349
+ test: async ({ id, values }) => {
143350
+ if (["cookie", "refreshToken"].some((key) => {
143351
+ const v = values[key];
143352
+ return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
143353
+ })) return {
143354
+ success: true,
143355
+ status: "inconclusive",
143356
+ error: "Save the account to check the sign-in"
143357
+ };
143358
+ const info = id === null ? null : deps.infoOf(id);
143359
+ if (info === null) return {
143360
+ success: false,
143361
+ status: "inconclusive",
143362
+ error: "Not connected: sign in first"
143363
+ };
143364
+ return info.status === "connected" ? {
143365
+ success: true,
143366
+ status: "validated"
143367
+ } : {
143368
+ success: false,
143369
+ status: "rejected",
143370
+ error: info.error ?? `status: ${info.status}`
143371
+ };
143372
+ }
143373
+ });
143374
+ }
143375
+ //#endregion
142720
143376
  //#region src/alexa-broker-store.ts
142721
143377
  function rowToEntry(id, data) {
142722
143378
  const parsed = AlexaBrokerEntrySchema.safeParse({
142723
143379
  id,
142724
143380
  name: data.name,
142725
- connection: data.connection,
142726
- integrationId: data.integrationId
143381
+ connection: {
143382
+ amazonDomain: data.amazonDomain,
143383
+ cookie: data.cookie,
143384
+ refreshToken: data.refreshToken,
143385
+ macDms: data.macDms,
143386
+ pollIntervalMs: data.pollIntervalMs
143387
+ },
143388
+ integrationId: data.integrationId,
143389
+ enabled: data.enabled
142727
143390
  });
142728
143391
  return parsed.success ? parsed.data : null;
142729
143392
  }
@@ -142751,16 +143414,20 @@ var AlexaBrokerStore = class AlexaBrokerStore {
142751
143414
  }
142752
143415
  return out;
142753
143416
  }
143417
+ async getById(id) {
143418
+ return (await this.listResolved()).find((entry) => entry.id === id) ?? null;
143419
+ }
142754
143420
  async replaceAll(entries) {
142755
143421
  const page = await this.kind.query({ limit: 1e3 });
142756
143422
  const nextIds = new Set(entries.map((e) => e.id));
142757
143423
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
142758
143424
  for (const entry of entries) {
142759
- const { id, name, connection, integrationId } = entry;
143425
+ const { id, name, connection, integrationId, enabled } = entry;
142760
143426
  const body = {
142761
143427
  name,
142762
- connection,
142763
- ...integrationId === void 0 ? {} : { integrationId }
143428
+ ...connection,
143429
+ ...integrationId === void 0 ? {} : { integrationId },
143430
+ ...enabled === void 0 ? {} : { enabled }
142764
143431
  };
142765
143432
  const parsed = alexaBrokersEntryKind.schema.safeParse(body);
142766
143433
  if (!parsed.success) throw new Error(`invalid Alexa broker '${id}': ${parsed.error.message}`);
@@ -144148,6 +144815,34 @@ var AlexaSpeakController = class {
144148
144815
  }
144149
144816
  };
144150
144817
  //#endregion
144818
+ //#region src/migrations/flatten-broker-records.ts
144819
+ /**
144820
+ * The Alexa broker records keep their account flat (planning#408 P3): the nested `connection`
144821
+ * object (`amazonDomain`, `cookie`, `refreshToken`, `macDms`, `pollIntervalMs`) becomes top-level
144822
+ * fields under the SAME record id (`alexa_NNN`). `macDms`, which used to be stored in plain text,
144823
+ * becomes a keychain secret. The work is `flattenNestedRecords`: every secret is read before
144824
+ * anything is written, read back from its new ref before the nested object goes, and the old refs
144825
+ * are deleted last.
144826
+ */
144827
+ var FLATTEN_BROKER_RECORDS_ID = "import-alexa/2026-11-04-flatten-broker-records";
144828
+ async function flattenBrokerRecords(ports) {
144829
+ return flattenNestedRecords(ports, {
144830
+ legacyKey: "connection",
144831
+ flatSecretKeys: [
144832
+ "cookie",
144833
+ "refreshToken",
144834
+ "macDms"
144835
+ ],
144836
+ toFlat: (legacy) => Object.fromEntries([
144837
+ "amazonDomain",
144838
+ "cookie",
144839
+ "refreshToken",
144840
+ "macDms",
144841
+ "pollIntervalMs"
144842
+ ].filter((key) => legacy[key] !== void 0).map((key) => [key, legacy[key]]))
144843
+ });
144844
+ }
144845
+ //#endregion
144151
144846
  //#region src/migrations/strip-echo-target-volume-and-mode.ts
144152
144847
  var RETIRED_KEYS = ["volume", "mode"];
144153
144848
  function isRecord(value) {
@@ -144197,6 +144892,21 @@ var ADDON_MIGRATIONS = [{
144197
144892
  fixtureMark: "the Echo id and the name the migration does not know"
144198
144893
  },
144199
144894
  procedure: stripEchoTargetVolumeAndMode
144895
+ }, {
144896
+ meta: {
144897
+ id: FLATTEN_BROKER_RECORDS_ID,
144898
+ owner: "import-alexa",
144899
+ level: "addon-state",
144900
+ seq: 2,
144901
+ dependsOn: [],
144902
+ store: "settings",
144903
+ scope: "cluster",
144904
+ onFailure: "block-owner",
144905
+ bodyKind: "procedure",
144906
+ description: "Move each Alexa account from a nested connection object to flat top-level fields under the same id, reading every secret back first",
144907
+ fixtureMark: "the integration link and the amazon domain of each account"
144908
+ },
144909
+ procedure: flattenBrokerRecords
144200
144910
  }];
144201
144911
  //#endregion
144202
144912
  //#region src/speak/icon-routes.ts
@@ -144251,7 +144961,6 @@ var RecordsSpeakSessionStore = class RecordsSpeakSessionStore {
144251
144961
  //#region src/addon.ts
144252
144962
  /** Default config — a fresh install starts with no accounts. */
144253
144963
  var DEFAULTS = {
144254
- brokers: [],
144255
144964
  proxyLogin: { proxyPort: 3456 },
144256
144965
  echoTargets: []
144257
144966
  };
@@ -144299,6 +145008,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144299
145008
  loginPlane = null;
144300
145009
  loginProxy = null;
144301
145010
  brokerStore = null;
145011
+ instances = null;
144302
145012
  activeBrokers = [];
144303
145013
  speak = null;
144304
145014
  constructor() {
@@ -144310,6 +145020,12 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144310
145020
  if (records === void 0) throw new Error("import-alexa: ctx.records is required (hub-only addon)");
144311
145021
  await records.declare(alexaRecordKinds);
144312
145022
  this.brokerStore = AlexaBrokerStore.of(records);
145023
+ this.instances = buildAlexaInstances(records, {
145024
+ mintId: () => this.requireRegistry().allocateId(),
145025
+ infoOf: (id) => this.registry?.get(id) ?? null,
145026
+ onChanged: (id) => this.syncBrokerRuntime(id)
145027
+ });
145028
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
144313
145029
  this.activeBrokers = await this.loadPersistedBrokers();
144314
145030
  this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
144315
145031
  dataDir: this.ctx.dataDir,
@@ -144354,12 +145070,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144354
145070
  deviceCode: this.deviceCodeLogin,
144355
145071
  commitCookie: (input) => this.commitCapturedCookie(input)
144356
145072
  }), ...speakIconRoutes()];
144357
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, {
144358
- readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
144359
- writeRows: async (rows) => {
144360
- await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
144361
- }
144362
- }));
145073
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
144363
145074
  const speak = new AlexaSpeakController({
144364
145075
  logger: this.ctx.logger.child("speak"),
144365
145076
  readConfig: () => this.config,
@@ -144389,6 +145100,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144389
145100
  capability: brokerCapability,
144390
145101
  provider: this.buildBrokerProvider()
144391
145102
  },
145103
+ {
145104
+ capability: integrationInstancesCapability,
145105
+ provider: this.requireInstances().toProvider()
145106
+ },
144392
145107
  {
144393
145108
  capability: deviceAdoptionCapability,
144394
145109
  provider: this.buildAdoptionProvider()
@@ -144403,19 +145118,65 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144403
145118
  }
144404
145119
  ] };
144405
145120
  }
144406
- async loadPersistedBrokers() {
144407
- const store = this.brokerStore;
144408
- if (store !== null) {
144409
- const fromRecords = await store.listResolved();
144410
- if (fromRecords.length > 0) return fromRecords;
145121
+ migrationPorts(records) {
145122
+ if (this.ctx.secrets === void 0) throw new Error("import-alexa: ctx.secrets is required");
145123
+ return {
145124
+ readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
145125
+ writeRows: async (rows) => {
145126
+ await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
145127
+ },
145128
+ ...flattenPortsOver(records.kind(alexaBrokersEntryKind))
145129
+ };
145130
+ }
145131
+ /**
145132
+ * Bring the runtime in line with one record after the instances page changed it: a removed
145133
+ * record stops its manager and cascades its devices, an off record only stops the manager, a
145134
+ * live one re-applies its connection or starts under the record's own id.
145135
+ */
145136
+ async syncBrokerRuntime(id) {
145137
+ const reg = this.requireRegistry();
145138
+ const entry = await this.requireBrokerStore().getById(id);
145139
+ this.activeBrokers = await this.loadPersistedBrokers();
145140
+ if (entry === null) {
145141
+ await reg.removeEntry(id);
145142
+ await this.cascadeRemoveDevicesForBroker(id);
145143
+ this.speak?.brokersChanged();
145144
+ return;
144411
145145
  }
144412
- return this.config.brokers;
145146
+ const configured = entry.connection.cookie.length > 0 || entry.connection.refreshToken.length > 0;
145147
+ if (entry.enabled === false || !configured) {
145148
+ await reg.removeEntry(id);
145149
+ this.speak?.brokersChanged();
145150
+ return;
145151
+ }
145152
+ try {
145153
+ if (reg.get(id) !== null) await reg.updateEntry(id, entry.connection);
145154
+ else await reg.createEntry(entry.name, entry.connection, {
145155
+ id,
145156
+ ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
145157
+ });
145158
+ } catch (err) {
145159
+ this.ctx.logger.warn("alexa instance: could not start the account", { meta: {
145160
+ brokerId: id,
145161
+ error: errMsg(err)
145162
+ } });
145163
+ }
145164
+ this.speak?.brokersChanged();
145165
+ }
145166
+ requireBrokerStore() {
145167
+ if (this.brokerStore === null) throw new Error("import-alexa: broker store not ready");
145168
+ return this.brokerStore;
145169
+ }
145170
+ requireInstances() {
145171
+ if (this.instances === null) throw new Error("import-alexa: instances not ready");
145172
+ return this.instances;
145173
+ }
145174
+ async loadPersistedBrokers() {
145175
+ return this.requireBrokerStore().listResolved();
144413
145176
  }
144414
145177
  async persistBrokerEntries(entries) {
144415
145178
  this.activeBrokers = [...entries];
144416
- const store = this.brokerStore;
144417
- if (store !== null) await store.replaceAll(entries);
144418
- else await this.updateGlobalSettings({ brokers: [...entries] });
145179
+ await this.requireBrokerStore().replaceAll(entries);
144419
145180
  this.speak?.brokersChanged();
144420
145181
  }
144421
145182
  async onConfigChanged() {
@@ -144434,6 +145195,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144434
145195
  } });
144435
145196
  }
144436
145197
  for (const entry of persisted) {
145198
+ if (entry.enabled === false) continue;
144437
145199
  if (entry.id && liveIds.has(entry.id)) {
144438
145200
  if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
144439
145201
  try {