@camstack/addon-google-services 0.2.36 → 0.2.37

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.
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
5981
5981
  var ZodFirstPartyTypeKind;
5982
5982
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5983
5983
  //#endregion
5984
- //#region ../types/dist/sleep-BKClVk0K.mjs
5984
+ //#region ../types/dist/sleep-BkzQdpzF.mjs
5985
5985
  /**
5986
5986
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5987
5987
  * window to float samples (D455).
@@ -6129,9 +6129,16 @@ function isSecretMarker(value) {
6129
6129
  const ref = value["$secret"];
6130
6130
  return typeof ref === "string" && ref.length > 0;
6131
6131
  }
6132
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6133
+ function isSecretMarkerLike(value) {
6134
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6135
+ }
6132
6136
  function isSecretClear(value) {
6133
6137
  return isRecord$7(value) && value["$secretClear"] === true;
6134
6138
  }
6139
+ function isSecretFieldView(value) {
6140
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6141
+ }
6135
6142
  /**
6136
6143
  * The UI view of a stored secret field value: a marker becomes "set", inline
6137
6144
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6781,11 +6788,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6781
6788
  }
6782
6789
  return out;
6783
6790
  }
6791
+ /**
6792
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6793
+ *
6794
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6795
+ * scope (global, one node, one device, one integration, one row of a business
6796
+ * collection) and the field path inside that scope — so saving the same field
6797
+ * twice updates the same ref, and the ref itself says which settings blob
6798
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6799
+ * copied onto another ref does not decrypt.
6800
+ *
6801
+ * Array items are addressed by ITEM ID, never by index
6802
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6803
+ *
6804
+ * Pure and browser-safe: no node builtins, no I/O.
6805
+ */
6806
+ /** The URI scheme of every keychain ref. */
6807
+ var SECRET_REF_SCHEME = "secret://";
6784
6808
  var GLOBAL_SCOPE = { kind: "global" };
6785
6809
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6786
6810
  function globalSecretScope() {
6787
6811
  return GLOBAL_SCOPE;
6788
6812
  }
6813
+ /**
6814
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6815
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6816
+ * addon running forked.
6817
+ */
6818
+ function normalizeSecretOwner(addonId) {
6819
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6820
+ }
6789
6821
  function encodeSegment(value) {
6790
6822
  return encodeURIComponent(value);
6791
6823
  }
@@ -6796,6 +6828,16 @@ function decodeSegment(value) {
6796
6828
  return null;
6797
6829
  }
6798
6830
  }
6831
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6832
+ function formatSecretScope(scope) {
6833
+ switch (scope.kind) {
6834
+ case "global": return "global";
6835
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6836
+ case "device": return `device/${scope.deviceId}`;
6837
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6838
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6839
+ }
6840
+ }
6799
6841
  /**
6800
6842
  * The field path of a value inside an array item, addressed by the item's id:
6801
6843
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6804,6 +6846,13 @@ function decodeSegment(value) {
6804
6846
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6805
6847
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6806
6848
  }
6849
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6850
+ function buildSecretRef(parts) {
6851
+ const owner = normalizeSecretOwner(parts.owner);
6852
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6853
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6854
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6855
+ }
6807
6856
  function parseScope(segments) {
6808
6857
  switch (segments[0]) {
6809
6858
  case "global": return {
@@ -8069,6 +8118,10 @@ var BaseAddon = class {
8069
8118
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8070
8119
  const patch = {};
8071
8120
  for (const key of keys) {
8121
+ if (tree.get(key)?.kind === "array") {
8122
+ patch[key] = [];
8123
+ continue;
8124
+ }
8072
8125
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8073
8126
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8074
8127
  }
@@ -8757,6 +8810,8 @@ var RECORD_BATCH_MAX = 1e3;
8757
8810
  var RECORD_QUERY_MAX = 1e3;
8758
8811
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8759
8812
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8813
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8814
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8760
8815
  /**
8761
8816
  * Secret fields of a record, addressed the way a Zod schema declares them
8762
8817
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
@@ -8793,6 +8848,112 @@ function secretArrayPrefixes(path) {
8793
8848
  }
8794
8849
  return prefixes;
8795
8850
  }
8851
+ function isRecord$4(value) {
8852
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8853
+ }
8854
+ function elementId(element, idKey, declared, prefix) {
8855
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8856
+ 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`);
8857
+ return id;
8858
+ }
8859
+ /**
8860
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8861
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8862
+ * result is a new body, `data` is untouched.
8863
+ */
8864
+ var REMOVE_LEAF = Symbol("remove-leaf");
8865
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8866
+ if (!isRecord$4(node)) return node;
8867
+ const segment = segments[index];
8868
+ if (segment === void 0) return node;
8869
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8870
+ const child = node[segment.key];
8871
+ const keys = [...walkedKeys, segment.key];
8872
+ const last = index === segments.length - 1;
8873
+ if (segment.each) {
8874
+ if (!Array.isArray(child)) return node;
8875
+ const prefix = keys.join(".");
8876
+ const idKey = arrayKeys[prefix];
8877
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8878
+ const mapped = child.map((element) => {
8879
+ const id = elementId(element, idKey, declared, prefix);
8880
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8881
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8882
+ });
8883
+ return {
8884
+ ...node,
8885
+ [segment.key]: mapped
8886
+ };
8887
+ }
8888
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8889
+ if (last) {
8890
+ const replacement = visit({
8891
+ declared,
8892
+ concrete,
8893
+ value: child
8894
+ });
8895
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8896
+ return {
8897
+ ...node,
8898
+ [segment.key]: replacement
8899
+ };
8900
+ }
8901
+ return {
8902
+ ...node,
8903
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8904
+ };
8905
+ }
8906
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8907
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8908
+ let current = data;
8909
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8910
+ return current;
8911
+ }
8912
+ /**
8913
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8914
+ * record's refs never collide with the refs a legacy collection of the same name
8915
+ * already holds during an adoption.
8916
+ */
8917
+ function recordSecretCollection(kind) {
8918
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8919
+ }
8920
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8921
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8922
+ return buildSecretRef({
8923
+ owner,
8924
+ scope: {
8925
+ kind: "row",
8926
+ collection: recordSecretCollection(kind),
8927
+ rowId
8928
+ },
8929
+ fieldPath: concretePath
8930
+ });
8931
+ }
8932
+ /**
8933
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8934
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8935
+ * token. The form hands it to the secret field, which reveals the value by ref
8936
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8937
+ * stored secret (the records door reads a view as KEEP).
8938
+ *
8939
+ * ONE implementation for the entries service and for every addon that serves
8940
+ * its own instances (`RecordIntegrationInstances`).
8941
+ */
8942
+ function isRecord$3(value) {
8943
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8944
+ }
8945
+ function secretViewsOf(owner, kind, id, decl, data) {
8946
+ if (decl.secretPaths.length === 0) return data;
8947
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8948
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8949
+ return {
8950
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8951
+ v: 1,
8952
+ set: true
8953
+ };
8954
+ });
8955
+ return isRecord$3(shown) ? shown : data;
8956
+ }
8796
8957
  /**
8797
8958
  * The share-view scope kinds a share-link token can carry. The hub mints
8798
8959
  * and validates them (`ShareViewGrantSchema` builds its
@@ -9015,7 +9176,7 @@ function defineRecordKinds(specs) {
9015
9176
  retention: spec.retention,
9016
9177
  deviceRemoval: spec.deviceRemoval,
9017
9178
  maxDataBytes: spec.maxDataBytes,
9018
- secretPaths: [...secretPaths],
9179
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9019
9180
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9020
9181
  transactFamily: spec.transactFamily,
9021
9182
  entry: spec.entry === void 0 ? void 0 : {
@@ -9060,6 +9221,7 @@ function defineEntryKind(name, spec) {
9060
9221
  ...spec.database === void 0 ? {} : { database: spec.database },
9061
9222
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9062
9223
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9224
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9063
9225
  entry: {
9064
9226
  title: spec.title,
9065
9227
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11735,7 +11897,7 @@ function userError(code, params, englishFallback) {
11735
11897
  return new CamstackError(code, params, englishFallback);
11736
11898
  }
11737
11899
  //#endregion
11738
- //#region ../types/dist/composition-BodkvOV0.mjs
11900
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11739
11901
  /**
11740
11902
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11741
11903
  *
@@ -12498,38 +12660,6 @@ function validateExpressionSource(src) {
12498
12660
  return null;
12499
12661
  }
12500
12662
  /**
12501
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12502
- *
12503
- * Standby of the control board is under 5 W; a motor or a heater is far
12504
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12505
- * a cycle, so running starts only after 30 s above the line.
12506
- *
12507
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12508
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12509
- * report "finished" in the middle of the program, so the stop dwell is
12510
- * 10 minutes on every type. The number is stored per type so one machine
12511
- * can move later without moving the others.
12512
- */
12513
- var APPLIANCE_TEMPLATE_ID = "appliance";
12514
- var APPLIANCE_KINDS = [
12515
- "washer",
12516
- "dryer",
12517
- "dishwasher"
12518
- ];
12519
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12520
- /**
12521
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12522
- *
12523
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12524
- * (D967), so the heater switches exactly as the plug does. Running is the
12525
- * plug being on; when a power reading is also named, running is the draw
12526
- * staying above the line instead (a plug left on with the stove's own
12527
- * thermostat open is not heating). 50 W is far above any standby and below
12528
- * the smallest radiator.
12529
- */
12530
- var HEATER_TEMPLATE_ID = "heater";
12531
- DeviceType.Heater;
12532
- /**
12533
12663
  * Statistics and integrals over a history read (D944).
12534
12664
  *
12535
12665
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12673,34 +12803,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12673
12803
  /** Actuator groups only. Readings leave this unset. */
12674
12804
  active: ActiveCriterionSchema.optional()
12675
12805
  }),
12676
- object({
12677
- kind: literal("appliance"),
12678
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12679
- appliance: _enum(APPLIANCE_KINDS),
12680
- thresholdWatts: number().positive(),
12681
- startDwellMs: number().int().nonnegative(),
12682
- stopDwellMs: number().int().nonnegative(),
12683
- member: HelperMemberSchema,
12684
- /**
12685
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12686
- * composition carries a `power-source` feature, so the hub can cut and restore
12687
- * that plug (power-source link of a NEW-target composition).
12688
- */
12689
- powerSwitch: HelperMemberSchema.optional(),
12690
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12691
- offSeconds: number().optional()
12692
- }),
12693
- object({
12694
- kind: literal("heater"),
12695
- templateId: literal(HEATER_TEMPLATE_ID),
12696
- /** The plug, read and commanded at `switch.on`. */
12697
- plug: HelperMemberSchema,
12698
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12699
- meter: HelperMemberSchema.optional(),
12700
- thresholdWatts: number().positive(),
12701
- startDwellMs: number().int().nonnegative(),
12702
- stopDwellMs: number().int().nonnegative()
12703
- }),
12704
12806
  object({
12705
12807
  kind: literal("statistic"),
12706
12808
  stat: _enum(STATISTIC_STATS),
@@ -12924,7 +13026,7 @@ var CompositionSchema = object({
12924
13026
  kind: literal("existing"),
12925
13027
  device: CompositionSourceRefSchema
12926
13028
  })]),
12927
- features: array(CompositionFeatureSchema).min(1).max(16),
13029
+ features: array(CompositionFeatureSchema).min(0).max(16),
12928
13030
  /**
12929
13031
  * Present when the block is a helper template. Absent on a free
12930
13032
  * composition (the room example, device 4603). The editor reads this,
@@ -13259,6 +13361,157 @@ function buildCapturingReply(envelope) {
13259
13361
  };
13260
13362
  return wrapper;
13261
13363
  }
13364
+ /** The most instances one addon lists (an integrations page, not a table). */
13365
+ var INSTANCES_LIST_MAX = 1e3;
13366
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13367
+ function topLevelSecretKeys(paths) {
13368
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13369
+ }
13370
+ var RecordIntegrationInstances = class {
13371
+ records;
13372
+ kind;
13373
+ hooks;
13374
+ handle;
13375
+ secretKeys;
13376
+ nameField;
13377
+ constructor(records, kind, hooks) {
13378
+ this.records = records;
13379
+ this.kind = kind;
13380
+ this.hooks = hooks;
13381
+ this.handle = records.kind(kind);
13382
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13383
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13384
+ }
13385
+ /**
13386
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13387
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13388
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13389
+ */
13390
+ async readResolved(id) {
13391
+ try {
13392
+ const record = await this.handle.getResolved(id);
13393
+ return record === null ? null : {
13394
+ id,
13395
+ body: record.data,
13396
+ enabled: record.data.enabled !== false,
13397
+ secrets: "resolved"
13398
+ };
13399
+ } catch (err) {
13400
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13401
+ const stored = await this.handle.get(id);
13402
+ if (stored === null) return null;
13403
+ return {
13404
+ id,
13405
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13406
+ enabled: stored.data.enabled !== false,
13407
+ secrets: "unavailable"
13408
+ };
13409
+ }
13410
+ }
13411
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13412
+ async getResolved(id) {
13413
+ return (await this.readResolved(id))?.body ?? null;
13414
+ }
13415
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13416
+ async listResolved() {
13417
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13418
+ const out = [];
13419
+ for (const record of page.records) {
13420
+ const read = await this.readResolved(record.id);
13421
+ if (read !== null) out.push(read);
13422
+ }
13423
+ return out;
13424
+ }
13425
+ viewOf(record) {
13426
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13427
+ const rawName = record.data[this.nameField];
13428
+ const kind = this.hooks.kindOf?.(record.data);
13429
+ const health = this.hooks.health?.(record.data, record.id);
13430
+ return {
13431
+ id: record.id,
13432
+ name: typeof rawName === "string" ? rawName : record.id,
13433
+ configured: this.hooks.configured(record.data),
13434
+ enabled: record.data.enabled !== false,
13435
+ formValues: shown,
13436
+ ...kind !== void 0 ? { kind } : {},
13437
+ ...health?.status !== void 0 ? { status: health.status } : {},
13438
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13439
+ };
13440
+ }
13441
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13442
+ ...this.hooks.description,
13443
+ mintsId: true
13444
+ };
13445
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13446
+ listInstances = async () => {
13447
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13448
+ };
13449
+ getInstance = async ({ id }) => {
13450
+ const record = await this.handle.get(id);
13451
+ return record === null ? null : this.viewOf(record);
13452
+ };
13453
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13454
+ createInstance = async ({ id, name, kind }) => {
13455
+ const minted = id ?? this.hooks.mintId?.({
13456
+ name,
13457
+ ...kind !== void 0 ? { kind } : {}
13458
+ });
13459
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13460
+ await this.handle.put({
13461
+ id: minted,
13462
+ data: this.hooks.initialBody(name, kind)
13463
+ }, { ifRev: 0 });
13464
+ await this.hooks.onChanged?.(minted);
13465
+ return { id: minted };
13466
+ };
13467
+ updateInstance = async ({ id, name, values }) => {
13468
+ const patch = {};
13469
+ for (const [key, value] of Object.entries(values)) {
13470
+ if (value === void 0) continue;
13471
+ if (!this.secretKeys.has(key)) patch[key] = value;
13472
+ else if (isSecretClear(value)) patch[key] = null;
13473
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13474
+ }
13475
+ if (name !== void 0) patch[this.nameField] = name;
13476
+ await this.handle.patch(id, patch);
13477
+ await this.hooks.onChanged?.(id);
13478
+ return { ok: true };
13479
+ };
13480
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13481
+ setInstanceEnabled = async ({ id, enabled }) => {
13482
+ await this.handle.patch(id, { enabled });
13483
+ await this.hooks.onChanged?.(id);
13484
+ return { ok: true };
13485
+ };
13486
+ /** The row and its keychain refs go together (the records door). */
13487
+ deleteInstance = async ({ id }) => {
13488
+ await this.handle.delete(id);
13489
+ await this.hooks.onChanged?.(id);
13490
+ return { ok: true };
13491
+ };
13492
+ testInstance = async (input) => {
13493
+ if (this.hooks.test === void 0) return {
13494
+ success: false,
13495
+ status: "unsupported",
13496
+ error: "This integration declares no connection test"
13497
+ };
13498
+ return this.hooks.test(input);
13499
+ };
13500
+ /** The flat provider object to hand to `registerProvider`. */
13501
+ toProvider() {
13502
+ return {
13503
+ describe: this.describe,
13504
+ instanceFormSchema: this.instanceFormSchema,
13505
+ listInstances: this.listInstances,
13506
+ getInstance: this.getInstance,
13507
+ createInstance: this.createInstance,
13508
+ updateInstance: this.updateInstance,
13509
+ setInstanceEnabled: this.setInstanceEnabled,
13510
+ deleteInstance: this.deleteInstance,
13511
+ testInstance: this.testInstance
13512
+ };
13513
+ }
13514
+ };
13262
13515
  var EncodeProfileSchema = object({
13263
13516
  video: object({
13264
13517
  codec: _enum([
@@ -48712,49 +48965,6 @@ function actuator(input) {
48712
48965
  };
48713
48966
  }
48714
48967
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
48715
- var PowerSourceRefusalReasonSchema = _enum([
48716
- "no-power-source",
48717
- "switch-missing",
48718
- "switch-offline",
48719
- "switch-unknown",
48720
- "restart-in-progress",
48721
- "switch-call-failed"
48722
- ]);
48723
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
48724
- accepted: literal(false),
48725
- reason: PowerSourceRefusalReasonSchema,
48726
- /** One line for the operator, naming the switch. */
48727
- message: string()
48728
- })]);
48729
- var PowerSourceInfoSchema = object({
48730
- /** False when the device has no power source linked; the rest is then empty. */
48731
- linked: boolean(),
48732
- switchDeviceId: number().nullable(),
48733
- switchName: string().nullable(),
48734
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
48735
- switchOn: boolean().nullable(),
48736
- switchOnline: boolean().nullable(),
48737
- restartInProgress: boolean(),
48738
- offSeconds: number().int()
48739
- });
48740
- object({
48741
- /** Null while the switch has not reported: unknown is never `false` (D393). */
48742
- switchOn: boolean().nullable(),
48743
- offSeconds: number().int(),
48744
- lastFetchedAt: number()
48745
- });
48746
- DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
48747
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
48748
- systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48749
- kind: "mutation",
48750
- auth: "admin"
48751
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48752
- kind: "mutation",
48753
- auth: "admin"
48754
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48755
- kind: "mutation",
48756
- auth: "admin"
48757
- });
48758
48968
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
48759
48969
  DeviceType.Camera;
48760
48970
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -48782,6 +48992,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
48782
48992
  }).optional()
48783
48993
  })) });
48784
48994
  /**
48995
+ * A saved Customize assignment (D936). The schema is the type: callers use
48996
+ * `z.infer`, they do not restate the shape.
48997
+ */
48998
+ var OverrideSourceSchema = object({
48999
+ addonId: string().min(1),
49000
+ stableId: string().min(1),
49001
+ cap: string().min(1),
49002
+ fieldPath: string().min(1)
49003
+ });
49004
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
49005
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
49006
+ var ThresholdTransformDraftSchema = object({
49007
+ kind: literal("threshold"),
49008
+ comparison: _enum(["gte", "lte"]),
49009
+ threshold: number(),
49010
+ startDwellMs: number().nonnegative(),
49011
+ stopDwellMs: number().nonnegative()
49012
+ });
49013
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
49014
+ IdentityTransformDraftSchema,
49015
+ InvertTransformDraftSchema,
49016
+ ThresholdTransformDraftSchema,
49017
+ object({
49018
+ kind: literal("linear"),
49019
+ scale: number(),
49020
+ offset: number(),
49021
+ clampMin: number().optional(),
49022
+ clampMax: number().optional()
49023
+ }),
49024
+ object({
49025
+ kind: literal("enum-map"),
49026
+ mapping: record(string(), string().nullable())
49027
+ }),
49028
+ object({ kind: literal("number") })
49029
+ ]);
49030
+ var ScalarOverrideDraftSchema = object({
49031
+ kind: literal("scalar"),
49032
+ cap: string().min(1),
49033
+ targetId: string().min(1),
49034
+ source: OverrideSourceSchema,
49035
+ transform: CustomizeTransformDraftSchema
49036
+ });
49037
+ var KeyedArrayOverrideDraftSchema = object({
49038
+ kind: literal("keyedArray"),
49039
+ cap: string().min(1),
49040
+ targetId: string().min(1),
49041
+ key: string().min(1),
49042
+ label: string().min(1),
49043
+ fieldPath: string().min(1),
49044
+ source: OverrideSourceSchema,
49045
+ transform: CustomizeTransformDraftSchema
49046
+ });
49047
+ var PulseOverrideDraftSchema = object({
49048
+ kind: literal("pulse"),
49049
+ cap: string().min(1),
49050
+ targetId: string().min(1),
49051
+ enabled: boolean(),
49052
+ debounceMs: number().nonnegative(),
49053
+ edges: array(_enum([
49054
+ "rising",
49055
+ "button",
49056
+ "native-event"
49057
+ ])).min(1).optional(),
49058
+ sources: array(OverrideSourceSchema).min(1)
49059
+ });
49060
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
49061
+ "in-addition",
49062
+ "confirm",
49063
+ "replace"
49064
+ ]).optional() });
49065
+ discriminatedUnion("kind", [
49066
+ ScalarOverrideDraftSchema,
49067
+ KeyedArrayOverrideDraftSchema,
49068
+ PulseOverrideDraftSchema,
49069
+ object({
49070
+ kind: literal("span"),
49071
+ cap: string().min(1),
49072
+ targetId: string().min(1),
49073
+ timeoutMs: number().nonnegative(),
49074
+ cooldownMs: number().nonnegative().optional(),
49075
+ sources: array(SpanSourceDraftSchema)
49076
+ })
49077
+ ]);
49078
+ object({
49079
+ targetId: string().min(1),
49080
+ configured: boolean(),
49081
+ source: _enum(["native", "assigned"]),
49082
+ state: _enum([
49083
+ "active",
49084
+ "stale",
49085
+ "released",
49086
+ "unavailable"
49087
+ ]),
49088
+ reason: string().nullable()
49089
+ });
49090
+ /**
49091
+ * What a running-threshold proposal says (planning#405). The schema is the
49092
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
49093
+ *
49094
+ * A proposal is read-only advice: it carries the numbers it was derived from
49095
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
49096
+ * WHY with a typed reason and the catalog defaults, never an invented number.
49097
+ */
49098
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
49099
+ var RunningProposalConfidenceSchema = _enum([
49100
+ "high",
49101
+ "medium",
49102
+ "low"
49103
+ ]);
49104
+ var RunningProposalInsufficientReasonSchema = _enum([
49105
+ "no-history",
49106
+ "span-too-short",
49107
+ "truncated",
49108
+ "no-idle-floor",
49109
+ "no-runs",
49110
+ "no-separation"
49111
+ ]);
49112
+ var RunningProposalReplaySchema = object({
49113
+ /** Intervals the real dwell expression held true over the window. */
49114
+ intervals: number().int().nonnegative(),
49115
+ /** Intervals outside every run, or a second interval inside one. */
49116
+ extra: number().int().nonnegative(),
49117
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
49118
+ missed: number().int().nonnegative()
49119
+ });
49120
+ var RunningProposalEvidenceSchema = object({
49121
+ tier: RunningProposalTierSchema,
49122
+ spanMs: number().nonnegative(),
49123
+ from: number(),
49124
+ to: number(),
49125
+ points: number().int().nonnegative(),
49126
+ runsFound: number().int().nonnegative(),
49127
+ pausesFound: number().int().nonnegative(),
49128
+ longestPauseMs: number().nonnegative(),
49129
+ idleFloorW: number(),
49130
+ noiseCeilingW: number(),
49131
+ plateauW: number(),
49132
+ replay: RunningProposalReplaySchema
49133
+ });
49134
+ var RunningProposalInsufficientEvidenceSchema = object({
49135
+ spanMs: number().nonnegative(),
49136
+ points: number().int().nonnegative(),
49137
+ runsFound: number().int().nonnegative()
49138
+ });
49139
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
49140
+ state: literal("proposal"),
49141
+ transform: ThresholdTransformDraftSchema,
49142
+ evidence: RunningProposalEvidenceSchema,
49143
+ confidence: RunningProposalConfidenceSchema,
49144
+ /** Why the confidence is not higher; empty when nothing held it back. */
49145
+ notes: array(string())
49146
+ }), object({
49147
+ state: literal("insufficient"),
49148
+ reason: RunningProposalInsufficientReasonSchema,
49149
+ evidence: RunningProposalInsufficientEvidenceSchema,
49150
+ /** The catalog defaults, offered as what stands when nothing better is known. */
49151
+ fallback: ThresholdTransformDraftSchema
49152
+ })]);
49153
+ /**
48785
49154
  * The report of a one-shot "move this owner's plaintext secrets into the
48786
49155
  * keychain" run (D878). COUNTS only - never a value, never a ref.
48787
49156
  *
@@ -49071,6 +49440,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49071
49440
  kind: "mutation",
49072
49441
  auth: "admin",
49073
49442
  access: "view"
49443
+ }), method(object({
49444
+ deviceId: number().int(),
49445
+ cap: string().min(1),
49446
+ fieldPath: string().min(1),
49447
+ deviceType: _enum(DeviceType).optional()
49448
+ }), RunningThresholdProposalSchema, {
49449
+ auth: "admin",
49450
+ access: "view"
49074
49451
  }), method(object({
49075
49452
  blockId: string().min(1),
49076
49453
  trace: AutomationRunTraceSchema.optional(),
@@ -49879,7 +50256,27 @@ method(object({
49879
50256
  * exposes only the COMMON surface: link state, supported device
49880
50257
  * kinds (so the UI can filter the device picker), and per-device
49881
50258
  * expose/unexpose.
49882
- */
50259
+ *
50260
+ * INSTANCES. One export addon can serve several independent targets (a
50261
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
50262
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
50263
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
50264
+ * default instance's); present = that instance; an id the provider does not
50265
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
50266
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
50267
+ * change always names the target it changes. A single-instance provider
50268
+ * accepts an absent id on a read or its own id, and refuses any other.
50269
+ */
50270
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
50271
+ var InstanceIdSchema$1 = string().min(1);
50272
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
50273
+ /** One independent export target of an addon. */
50274
+ var ExportInstanceSchema = object({
50275
+ instanceId: InstanceIdSchema$1,
50276
+ name: string(),
50277
+ enabled: boolean(),
50278
+ kind: string().optional()
50279
+ });
49883
50280
  var LinkStateSchema = _enum([
49884
50281
  "unlinked",
49885
50282
  "linked",
@@ -49930,10 +50327,14 @@ var ExposedDeviceSchema = object({
49930
50327
  capabilities: array(string()).optional()
49931
50328
  });
49932
50329
  var ExposeInputSchema = object({
50330
+ instanceId: InstanceIdSchema$1,
49933
50331
  deviceId: string(),
49934
50332
  capabilities: array(string()).optional()
49935
50333
  });
49936
- var UnexposeInputSchema = object({ deviceId: string() });
50334
+ var UnexposeInputSchema = object({
50335
+ instanceId: InstanceIdSchema$1,
50336
+ deviceId: string()
50337
+ });
49937
50338
  /**
49938
50339
  * What an exporter may expose besides devices (D968). An action and a
49939
50340
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -49962,15 +50363,21 @@ var ExposedEntitySchema = object({
49962
50363
  entity: ExportEntityRefSchema,
49963
50364
  exposedAs: string().optional()
49964
50365
  });
49965
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
49966
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
49967
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50366
+ var ExposeEntityInputSchema = object({
50367
+ instanceId: InstanceIdSchema$1,
50368
+ entity: ExportEntityRefSchema
50369
+ });
50370
+ var UnexposeEntityInputSchema = object({
50371
+ instanceId: InstanceIdSchema$1,
50372
+ entity: ExportEntityRefSchema
50373
+ });
50374
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
49968
50375
  kind: "mutation",
49969
50376
  auth: "admin"
49970
50377
  }), method(UnexposeInputSchema, _void(), {
49971
50378
  kind: "mutation",
49972
50379
  auth: "admin"
49973
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50380
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
49974
50381
  kind: "mutation",
49975
50382
  auth: "admin"
49976
50383
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -53171,15 +53578,15 @@ var FanPresetCaptureResultSchema = object({
53171
53578
  items: array(FanCaptureItemSchema),
53172
53579
  omitted: array(FanCaptureOmissionSchema)
53173
53580
  });
53174
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
53581
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
53175
53582
  DeviceType.Fan;
53176
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53583
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53177
53584
  kind: "mutation",
53178
53585
  auth: "admin"
53179
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53586
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53180
53587
  kind: "mutation",
53181
53588
  auth: "protected"
53182
- }), method(deviceInput$1.extend({
53589
+ }), method(deviceInput$2.extend({
53183
53590
  fields: array(FanCaptureFieldSchema).optional(),
53184
53591
  steps: array(ActionStepSchema).max(20).default([])
53185
53592
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -53703,13 +54110,50 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53703
54110
  */
53704
54111
  var ConfigSchemaPassthrough$3 = custom();
53705
54112
  var IntegrationInstanceIdSchema = string().min(1);
54113
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54114
+ /**
54115
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54116
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54117
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
54118
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
54119
+ * instance id.
54120
+ */
54121
+ var IntegrationInstanceKindSchema = object({
54122
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
54123
+ id: string().min(1),
54124
+ label: string().min(1),
54125
+ /** The kind signs in through a server-held interactive session. */
54126
+ signIn: object({
54127
+ basePath: string().min(1),
54128
+ methods: array(_enum(["device-code", "proxy"])).optional()
54129
+ }).optional(),
54130
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
54131
+ connectPath: string().min(1).optional(),
54132
+ /** Relative hub path to disconnect that account. */
54133
+ disconnectPath: string().min(1).optional()
54134
+ });
53706
54135
  var IntegrationInstanceDescriptionSchema = object({
53707
54136
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
53708
- instanceMode: _enum(["single", "multiple"]),
54137
+ instanceMode: IntegrationInstanceModeSchema,
53709
54138
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
53710
54139
  kind: string(),
53711
54140
  /** True when `testInstance` really probes the remote. */
53712
- canTest: boolean()
54141
+ canTest: boolean(),
54142
+ /**
54143
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
54144
+ * Add flow asks which first and sends it as `createInstance.kind`.
54145
+ */
54146
+ kinds: array(IntegrationInstanceKindSchema).optional(),
54147
+ /**
54148
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
54149
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
54150
+ */
54151
+ allowRestoreId: boolean().optional(),
54152
+ /**
54153
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
54154
+ * send an id (the UI then mints an opaque one).
54155
+ */
54156
+ mintsId: boolean().optional()
53713
54157
  });
53714
54158
  var IntegrationInstanceStatusSchema = object({
53715
54159
  state: _enum([
@@ -53718,7 +54162,9 @@ var IntegrationInstanceStatusSchema = object({
53718
54162
  "error",
53719
54163
  "unknown"
53720
54164
  ]),
53721
- detail: string().optional()
54165
+ detail: string().optional(),
54166
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
54167
+ label: string().optional()
53722
54168
  });
53723
54169
  var IntegrationInstanceSchema = object({
53724
54170
  id: IntegrationInstanceIdSchema,
@@ -53730,9 +54176,13 @@ var IntegrationInstanceSchema = object({
53730
54176
  * (`enabled !== false`, so a row written before the field existed is on).
53731
54177
  */
53732
54178
  enabled: boolean(),
54179
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
54180
+ kind: string().optional(),
53733
54181
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
53734
54182
  formValues: record(string(), unknown()),
53735
- status: IntegrationInstanceStatusSchema.optional()
54183
+ status: IntegrationInstanceStatusSchema.optional(),
54184
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
54185
+ lastCheckedAt: number().optional()
53736
54186
  });
53737
54187
  var IntegrationInstancePatchSchema = object({
53738
54188
  id: IntegrationInstanceIdSchema,
@@ -53740,31 +54190,57 @@ var IntegrationInstancePatchSchema = object({
53740
54190
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
53741
54191
  values: record(string(), unknown())
53742
54192
  });
53743
- 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({
53744
- id: IntegrationInstanceIdSchema,
53745
- name: string().min(1)
53746
- }), object({ id: string() }), {
53747
- kind: "mutation",
53748
- auth: "admin"
53749
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
53750
- kind: "mutation",
53751
- auth: "admin"
53752
- }), method(object({
53753
- id: IntegrationInstanceIdSchema,
53754
- enabled: boolean()
53755
- }), object({ ok: literal(true) }), {
53756
- kind: "mutation",
53757
- auth: "admin"
53758
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
53759
- kind: "mutation",
53760
- auth: "admin"
53761
- }), optionalMethod(object({
53762
- id: IntegrationInstanceIdSchema.nullable(),
53763
- values: record(string(), unknown())
53764
- }), TestConnectionResultSchema$1, {
53765
- kind: "mutation",
53766
- auth: "admin"
53767
- });
54193
+ var integrationInstancesCapability = {
54194
+ name: "integration-instances",
54195
+ scope: "system",
54196
+ mode: "collection",
54197
+ methods: {
54198
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
54199
+ /** The form; `id: null` is the create form. */
54200
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54201
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54202
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54203
+ /**
54204
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
54205
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
54206
+ */
54207
+ createInstance: method(object({
54208
+ id: IntegrationInstanceIdSchema.optional(),
54209
+ name: string().min(1),
54210
+ kind: string().min(1).optional()
54211
+ }), object({ id: string() }), {
54212
+ kind: "mutation",
54213
+ auth: "admin"
54214
+ }),
54215
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
54216
+ kind: "mutation",
54217
+ auth: "admin"
54218
+ }),
54219
+ /**
54220
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
54221
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
54222
+ */
54223
+ setInstanceEnabled: method(object({
54224
+ id: IntegrationInstanceIdSchema,
54225
+ enabled: boolean()
54226
+ }), object({ ok: literal(true) }), {
54227
+ kind: "mutation",
54228
+ auth: "admin"
54229
+ }),
54230
+ /** Delete the record and its keychain refs together. */
54231
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
54232
+ kind: "mutation",
54233
+ auth: "admin"
54234
+ }),
54235
+ testInstance: optionalMethod(object({
54236
+ id: IntegrationInstanceIdSchema.nullable(),
54237
+ values: record(string(), unknown())
54238
+ }), TestConnectionResultSchema$1, {
54239
+ kind: "mutation",
54240
+ auth: "admin"
54241
+ })
54242
+ }
54243
+ };
53768
54244
  /**
53769
54245
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
53770
54246
  * session (server-initiated offer pattern, identical to the main
@@ -55563,6 +56039,11 @@ var LogEntrySchema = object({
55563
56039
  * flight, and not asking for the next slice ends the scan.
55564
56040
  */
55565
56041
  var LogReadQuerySchema = object({
56042
+ /**
56043
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
56044
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
56045
+ */
56046
+ instanceId: string().min(1).max(200).optional(),
55566
56047
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
55567
56048
  requestId: string().min(1).max(64).optional(),
55568
56049
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -55625,6 +56106,8 @@ var LogReadResultSchema = object({
55625
56106
  /** True when `cancelRead` ended this slice early. */
55626
56107
  cancelled: boolean().optional()
55627
56108
  });
56109
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
56110
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
55628
56111
  var LogReadCapabilitiesSchema = object({
55629
56112
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
55630
56113
  readable: boolean(),
@@ -55643,7 +56126,7 @@ var LogReadCapabilitiesSchema = object({
55643
56126
  /** How far back the store reaches, when the provider knows (days). */
55644
56127
  retentionDays: number().nullable()
55645
56128
  });
55646
- 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() }), {
56129
+ 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() }), {
55647
56130
  kind: "mutation",
55648
56131
  auth: "admin"
55649
56132
  });
@@ -67215,6 +67698,49 @@ object({
67215
67698
  DeviceType.PetFeeder,
67216
67699
  DeviceType.Heater
67217
67700
  ].filter((type) => type !== DeviceType.Sensor);
67701
+ var PowerSourceRefusalReasonSchema = _enum([
67702
+ "no-power-source",
67703
+ "switch-missing",
67704
+ "switch-offline",
67705
+ "switch-unknown",
67706
+ "restart-in-progress",
67707
+ "switch-call-failed"
67708
+ ]);
67709
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
67710
+ accepted: literal(false),
67711
+ reason: PowerSourceRefusalReasonSchema,
67712
+ /** One line for the operator, naming the switch. */
67713
+ message: string()
67714
+ })]);
67715
+ var PowerSourceInfoSchema = object({
67716
+ /** False when the device has no power source linked; the rest is then empty. */
67717
+ linked: boolean(),
67718
+ switchDeviceId: number().nullable(),
67719
+ switchName: string().nullable(),
67720
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
67721
+ switchOn: boolean().nullable(),
67722
+ switchOnline: boolean().nullable(),
67723
+ restartInProgress: boolean(),
67724
+ offSeconds: number().int()
67725
+ });
67726
+ object({
67727
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
67728
+ switchOn: boolean().nullable(),
67729
+ offSeconds: number().int(),
67730
+ lastFetchedAt: number()
67731
+ });
67732
+ DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
67733
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
67734
+ systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67735
+ kind: "mutation",
67736
+ auth: "admin"
67737
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67738
+ kind: "mutation",
67739
+ auth: "admin"
67740
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67741
+ kind: "mutation",
67742
+ auth: "admin"
67743
+ });
67218
67744
  /**
67219
67745
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
67220
67746
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -75281,6 +75807,25 @@ function buildDescriptor(capName, kind) {
75281
75807
  * contributes one entry per bound cap present in this map.
75282
75808
  */
75283
75809
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
75810
+ /**
75811
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
75812
+ * shared by the admin UI and the viewer. A role is data on the wire; each
75813
+ * client resolves its name from `label` (English and Italian side by side) and
75814
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
75815
+ * server sends an id, a client maps it to its own icon set.
75816
+ *
75817
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
75818
+ * pictogram): a client that can draw text shows it, one that cannot falls back
75819
+ * to `icon`.
75820
+ */
75821
+ function digit(n) {
75822
+ return {
75823
+ label: t(n, n),
75824
+ icon: "hash",
75825
+ legend: n
75826
+ };
75827
+ }
75828
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
75284
75829
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
75285
75830
  var btn = (idSuffix, row, col, init) => ({
75286
75831
  idSuffix,
@@ -75408,6 +75953,34 @@ discriminatedUnion("kind", [object({
75408
75953
  kind: literal("pronto"),
75409
75954
  code: string()
75410
75955
  })]);
75956
+ /**
75957
+ * Propose a running threshold from a device's own power history (planning#405).
75958
+ *
75959
+ * Pure and deterministic: the same series gives the same answer. The series is
75960
+ * a HELD-VALUE step series — a point's value stands until the next point — which
75961
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
75962
+ * count of samples.
75963
+ *
75964
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
75965
+ * near F. A device that is not near one level most of the time has no floor.
75966
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
75967
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
75968
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
75969
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
75970
+ * grid a person would type; stop dwell covers the longest pause; start dwell
75971
+ * covers the longest standby blip above T.
75972
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
75973
+ * run must hold exactly one true interval and nothing may flap outside.
75974
+ */
75975
+ var SECOND_MS = 1e3;
75976
+ var MINUTE_MS$1 = 60 * SECOND_MS;
75977
+ 24 * (60 * MINUTE_MS$1);
75978
+ 10 * MINUTE_MS$1;
75979
+ 30 * SECOND_MS;
75980
+ 60 * SECOND_MS;
75981
+ 20 * MINUTE_MS$1;
75982
+ 10 * SECOND_MS;
75983
+ 10 * SECOND_MS;
75411
75984
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75412
75985
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
75413
75986
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -76949,6 +77522,13 @@ Object.freeze({
76949
77522
  access: "create",
76950
77523
  role: "admin"
76951
77524
  },
77525
+ "coreBlocks.proposeRunningThreshold": {
77526
+ capName: "core-blocks",
77527
+ capScope: "system",
77528
+ addonId: null,
77529
+ access: "view",
77530
+ role: "admin"
77531
+ },
76952
77532
  "coreBlocks.reportAutomationRun": {
76953
77533
  capName: "core-blocks",
76954
77534
  capScope: "system",
@@ -77460,6 +78040,13 @@ Object.freeze({
77460
78040
  access: "view",
77461
78041
  role: "admin"
77462
78042
  },
78043
+ "deviceExport.listInstances": {
78044
+ capName: "device-export",
78045
+ capScope: "system",
78046
+ addonId: null,
78047
+ access: "view",
78048
+ role: "admin"
78049
+ },
77463
78050
  "deviceExport.listSupportedDeviceKinds": {
77464
78051
  capName: "device-export",
77465
78052
  capScope: "system",
@@ -86407,6 +86994,11 @@ Object.freeze({
86407
86994
  form: "single",
86408
86995
  optional: false
86409
86996
  }],
86997
+ "coreBlocks.proposeRunningThreshold": [{
86998
+ name: "deviceId",
86999
+ form: "single",
87000
+ optional: false
87001
+ }],
86410
87002
  "cover.close": [{
86411
87003
  name: "deviceId",
86412
87004
  form: "single",
@@ -89070,99 +89662,6 @@ Object.freeze({
89070
89662
  "network-access": "ingress",
89071
89663
  "smtp-provider": "email"
89072
89664
  });
89073
- /**
89074
- * A saved Customize assignment (D936). The schema is the type: callers use
89075
- * `z.infer`, they do not restate the shape.
89076
- */
89077
- var OverrideSourceSchema = object({
89078
- addonId: string().min(1),
89079
- stableId: string().min(1),
89080
- cap: string().min(1),
89081
- fieldPath: string().min(1)
89082
- });
89083
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
89084
- object({ kind: literal("identity") }),
89085
- object({ kind: literal("invert") }),
89086
- object({
89087
- kind: literal("threshold"),
89088
- comparison: _enum(["gte", "lte"]),
89089
- threshold: number(),
89090
- startDwellMs: number().nonnegative(),
89091
- stopDwellMs: number().nonnegative()
89092
- }),
89093
- object({
89094
- kind: literal("linear"),
89095
- scale: number(),
89096
- offset: number(),
89097
- clampMin: number().optional(),
89098
- clampMax: number().optional()
89099
- }),
89100
- object({
89101
- kind: literal("enum-map"),
89102
- mapping: record(string(), string().nullable())
89103
- }),
89104
- object({ kind: literal("number") })
89105
- ]);
89106
- var ScalarOverrideDraftSchema = object({
89107
- kind: literal("scalar"),
89108
- cap: string().min(1),
89109
- targetId: string().min(1),
89110
- source: OverrideSourceSchema,
89111
- transform: CustomizeTransformDraftSchema
89112
- });
89113
- var KeyedArrayOverrideDraftSchema = object({
89114
- kind: literal("keyedArray"),
89115
- cap: string().min(1),
89116
- targetId: string().min(1),
89117
- key: string().min(1),
89118
- label: string().min(1),
89119
- fieldPath: string().min(1),
89120
- source: OverrideSourceSchema,
89121
- transform: CustomizeTransformDraftSchema
89122
- });
89123
- var PulseOverrideDraftSchema = object({
89124
- kind: literal("pulse"),
89125
- cap: string().min(1),
89126
- targetId: string().min(1),
89127
- enabled: boolean(),
89128
- debounceMs: number().nonnegative(),
89129
- edges: array(_enum([
89130
- "rising",
89131
- "button",
89132
- "native-event"
89133
- ])).min(1).optional(),
89134
- sources: array(OverrideSourceSchema).min(1)
89135
- });
89136
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
89137
- "in-addition",
89138
- "confirm",
89139
- "replace"
89140
- ]).optional() });
89141
- discriminatedUnion("kind", [
89142
- ScalarOverrideDraftSchema,
89143
- KeyedArrayOverrideDraftSchema,
89144
- PulseOverrideDraftSchema,
89145
- object({
89146
- kind: literal("span"),
89147
- cap: string().min(1),
89148
- targetId: string().min(1),
89149
- timeoutMs: number().nonnegative(),
89150
- cooldownMs: number().nonnegative().optional(),
89151
- sources: array(SpanSourceDraftSchema)
89152
- })
89153
- ]);
89154
- object({
89155
- targetId: string().min(1),
89156
- configured: boolean(),
89157
- source: _enum(["native", "assigned"]),
89158
- state: _enum([
89159
- "active",
89160
- "stale",
89161
- "released",
89162
- "unavailable"
89163
- ]),
89164
- reason: string().nullable()
89165
- });
89166
89665
  var G711_SCALE_CORRECTION_DB = {
89167
89666
  PCMU: 20 * Math.log10(4),
89168
89667
  PCMA: 20 * Math.log10(8)
@@ -91546,6 +92045,87 @@ var googleBrokersEntryKind = defineEntryKind("brokers", {
91546
92045
  writeTrigger: "an operator adds or edits a Google OAuth broker"
91547
92046
  });
91548
92047
  //#endregion
92048
+ //#region src/google-instances.ts
92049
+ /**
92050
+ * The Google accounts as `integration-instances` (planning#408 P3): one instance per broker
92051
+ * record, over the SAME `brokers` records and ids the `broker` cap serves (`google_NNN`), so
92052
+ * nothing moves. The OAuth session lives beside the record (`oauthAccounts`), so an instance's
92053
+ * status is the registry's answer and a change re-seeds the registry.
92054
+ */
92055
+ function asTestResult(r) {
92056
+ return r.ok ? {
92057
+ success: true,
92058
+ status: "validated",
92059
+ ...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
92060
+ } : {
92061
+ success: false,
92062
+ status: "rejected",
92063
+ error: r.error ?? "Google rejected the check"
92064
+ };
92065
+ }
92066
+ function buildGoogleInstances(records, deps) {
92067
+ return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
92068
+ description: {
92069
+ instanceMode: "multiple",
92070
+ kind: "account",
92071
+ canTest: true,
92072
+ kinds: [{
92073
+ id: GOOGLE_BROKER_KIND,
92074
+ label: GOOGLE_BROKER_KIND_LABEL,
92075
+ connectPath: deps.connectPath,
92076
+ disconnectPath: deps.disconnectPath
92077
+ }],
92078
+ allowRestoreId: true
92079
+ },
92080
+ formSchema: () => {
92081
+ const full = buildGoogleBrokerSettingsSchema(deps.publicOrigin());
92082
+ return {
92083
+ ...full,
92084
+ sections: full.sections.filter((s) => s.id !== "google-calendars-import")
92085
+ };
92086
+ },
92087
+ initialBody: (name) => ({
92088
+ name,
92089
+ clientId: "",
92090
+ clientSecret: "",
92091
+ calendarFilter: "",
92092
+ fromAddress: "",
92093
+ fromName: "",
92094
+ calendarIds: []
92095
+ }),
92096
+ mintId: deps.mintId,
92097
+ kindOf: () => GOOGLE_BROKER_KIND,
92098
+ health: (_body, id) => {
92099
+ const session = deps.session(id);
92100
+ if (session === "connected") return { status: {
92101
+ state: "ok",
92102
+ label: "Signed in"
92103
+ } };
92104
+ if (session === "reauth") return { status: {
92105
+ state: "error",
92106
+ label: "Sign in again",
92107
+ detail: "Google session expired"
92108
+ } };
92109
+ return { status: {
92110
+ state: "unknown",
92111
+ label: "Not signed in"
92112
+ } };
92113
+ },
92114
+ configured: (body) => body.clientId.length > 0 && (body.clientSecret ?? "").length > 0,
92115
+ onChanged: deps.onChanged,
92116
+ test: async ({ id, values }) => {
92117
+ const clientId = values["clientId"];
92118
+ const secret = values["clientSecret"];
92119
+ if (typeof clientId === "string" && clientId.length > 0 && typeof secret === "string" && secret.length > 0 && secret !== "__camstack_redacted__") return asTestResult(await deps.testClient(clientId, secret));
92120
+ if (id === null) return asTestResult({
92121
+ ok: false,
92122
+ error: "Enter the Client ID and Client secret first"
92123
+ });
92124
+ return asTestResult(await deps.testSaved(id));
92125
+ }
92126
+ });
92127
+ }
92128
+ //#endregion
91549
92129
  //#region src/migrations/calendar-ids-to-brokers.ts
91550
92130
  /**
91551
92131
  * The calendars a Google account imports are a fact of THAT account (D985): each broker record
@@ -91776,6 +92356,7 @@ var GoogleServicesAddon = class extends BaseAddon {
91776
92356
  oauthAccountsHandle = null;
91777
92357
  registry = new GoogleBrokerRegistry();
91778
92358
  brokerStore = null;
92359
+ instances = null;
91779
92360
  activeBrokers = [];
91780
92361
  /** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
91781
92362
  pendingStates = /* @__PURE__ */ new Map();
@@ -91823,6 +92404,7 @@ var GoogleServicesAddon = class extends BaseAddon {
91823
92404
  if (records === void 0) throw new Error("google-services: ctx.records is required (hub-only addon)");
91824
92405
  await records.declare({ brokers: googleBrokersEntryKind });
91825
92406
  this.brokerStore = GoogleBrokerStore.of(records);
92407
+ this.instances = this.buildInstances(records);
91826
92408
  this.activeBrokers = await this.loadPersistedBrokers();
91827
92409
  if ((await this.brokerStore.listResolved()).length === 0 && this.config.brokers.length > 0) {
91828
92410
  await this.brokerStore.replaceAll(this.config.brokers);
@@ -91885,6 +92467,10 @@ var GoogleServicesAddon = class extends BaseAddon {
91885
92467
  capability: brokerCapability,
91886
92468
  provider: this.buildBrokerProvider()
91887
92469
  },
92470
+ {
92471
+ capability: integrationInstancesCapability,
92472
+ provider: this.requireInstances().toProvider()
92473
+ },
91888
92474
  {
91889
92475
  capability: calendarSourceCapability,
91890
92476
  provider: calendarProvider
@@ -91904,6 +92490,35 @@ var GoogleServicesAddon = class extends BaseAddon {
91904
92490
  this.activeBrokers = await this.loadPersistedBrokers();
91905
92491
  this.registry.seedFrom(this.activeBrokers, accounts);
91906
92492
  }
92493
+ requireInstances() {
92494
+ if (this.instances === null) throw new Error("google-services: instances used before initialize");
92495
+ return this.instances;
92496
+ }
92497
+ /** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
92498
+ buildInstances(records) {
92499
+ const brokerProvider = this.buildBrokerProvider();
92500
+ return buildGoogleInstances(records, {
92501
+ connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
92502
+ disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`,
92503
+ publicOrigin: () => this.publicOrigin,
92504
+ mintId: () => this.registry.allocateId(),
92505
+ session: (id) => this.registry.isConnected(id) ? "connected" : this.registry.needsReauth(id) ? "reauth" : "none",
92506
+ onChanged: async () => {
92507
+ const accounts = this.registry.listOauthAccounts();
92508
+ this.activeBrokers = await this.loadPersistedBrokers();
92509
+ this.registry.seedFrom(this.activeBrokers, accounts);
92510
+ await this.persistOauthAccounts();
92511
+ },
92512
+ testSaved: async (id) => brokerProvider.testConnection({ id }),
92513
+ testClient: async (clientId, clientSecret) => brokerProvider.testSettings({
92514
+ kind: GOOGLE_BROKER_KIND,
92515
+ settings: {
92516
+ clientId,
92517
+ clientSecret
92518
+ }
92519
+ })
92520
+ });
92521
+ }
91907
92522
  buildBrokerProvider() {
91908
92523
  const ownerAddonId = this.ctx.id;
91909
92524
  const owns = (id) => this.registry.getEntry(id) !== null;