@camstack/addon-google-services 0.2.36 → 0.2.38

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.
@@ -5985,7 +5985,7 @@ var ZodIssueCode = {
5985
5985
  var ZodFirstPartyTypeKind;
5986
5986
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5987
5987
  //#endregion
5988
- //#region ../types/dist/sleep-BKClVk0K.mjs
5988
+ //#region ../types/dist/sleep-BaWhXZFm.mjs
5989
5989
  /**
5990
5990
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5991
5991
  * window to float samples (D455).
@@ -6133,9 +6133,16 @@ function isSecretMarker(value) {
6133
6133
  const ref = value["$secret"];
6134
6134
  return typeof ref === "string" && ref.length > 0;
6135
6135
  }
6136
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6137
+ function isSecretMarkerLike(value) {
6138
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6139
+ }
6136
6140
  function isSecretClear(value) {
6137
6141
  return isRecord$7(value) && value["$secretClear"] === true;
6138
6142
  }
6143
+ function isSecretFieldView(value) {
6144
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6145
+ }
6139
6146
  /**
6140
6147
  * The UI view of a stored secret field value: a marker becomes "set", inline
6141
6148
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6785,11 +6792,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6785
6792
  }
6786
6793
  return out;
6787
6794
  }
6795
+ /**
6796
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6797
+ *
6798
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6799
+ * scope (global, one node, one device, one integration, one row of a business
6800
+ * collection) and the field path inside that scope — so saving the same field
6801
+ * twice updates the same ref, and the ref itself says which settings blob
6802
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6803
+ * copied onto another ref does not decrypt.
6804
+ *
6805
+ * Array items are addressed by ITEM ID, never by index
6806
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6807
+ *
6808
+ * Pure and browser-safe: no node builtins, no I/O.
6809
+ */
6810
+ /** The URI scheme of every keychain ref. */
6811
+ var SECRET_REF_SCHEME = "secret://";
6788
6812
  var GLOBAL_SCOPE = { kind: "global" };
6789
6813
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6790
6814
  function globalSecretScope() {
6791
6815
  return GLOBAL_SCOPE;
6792
6816
  }
6817
+ /**
6818
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6819
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6820
+ * addon running forked.
6821
+ */
6822
+ function normalizeSecretOwner(addonId) {
6823
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6824
+ }
6793
6825
  function encodeSegment(value) {
6794
6826
  return encodeURIComponent(value);
6795
6827
  }
@@ -6800,6 +6832,16 @@ function decodeSegment(value) {
6800
6832
  return null;
6801
6833
  }
6802
6834
  }
6835
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6836
+ function formatSecretScope(scope) {
6837
+ switch (scope.kind) {
6838
+ case "global": return "global";
6839
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6840
+ case "device": return `device/${scope.deviceId}`;
6841
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6842
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6843
+ }
6844
+ }
6803
6845
  /**
6804
6846
  * The field path of a value inside an array item, addressed by the item's id:
6805
6847
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6808,6 +6850,13 @@ function decodeSegment(value) {
6808
6850
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6809
6851
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6810
6852
  }
6853
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6854
+ function buildSecretRef(parts) {
6855
+ const owner = normalizeSecretOwner(parts.owner);
6856
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6857
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6858
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6859
+ }
6811
6860
  function parseScope(segments) {
6812
6861
  switch (segments[0]) {
6813
6862
  case "global": return {
@@ -8073,6 +8122,10 @@ var BaseAddon = class {
8073
8122
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8074
8123
  const patch = {};
8075
8124
  for (const key of keys) {
8125
+ if (tree.get(key)?.kind === "array") {
8126
+ patch[key] = [];
8127
+ continue;
8128
+ }
8076
8129
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8077
8130
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8078
8131
  }
@@ -8761,11 +8814,17 @@ var RECORD_BATCH_MAX = 1e3;
8761
8814
  var RECORD_QUERY_MAX = 1e3;
8762
8815
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8763
8816
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8817
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8818
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8764
8819
  /**
8765
8820
  * Secret fields of a record, addressed the way a Zod schema declares them
8766
8821
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8767
8822
  * (`auth.accessToken`, `headers[].value`).
8768
8823
  *
8824
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8825
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8826
+ * key releases exactly that key's secret.
8827
+ *
8769
8828
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8770
8829
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8771
8830
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8778,11 +8837,13 @@ function parseSecretPath(path) {
8778
8837
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8779
8838
  return path.split(".").map((raw) => {
8780
8839
  const each = raw.endsWith("[]");
8781
- const key = each ? raw.slice(0, -2) : raw;
8840
+ const map = raw.endsWith("{}");
8841
+ const key = each || map ? raw.slice(0, -2) : raw;
8782
8842
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8783
8843
  return {
8784
8844
  key,
8785
- each
8845
+ each,
8846
+ map
8786
8847
  };
8787
8848
  });
8788
8849
  }
@@ -8797,6 +8858,128 @@ function secretArrayPrefixes(path) {
8797
8858
  }
8798
8859
  return prefixes;
8799
8860
  }
8861
+ function isRecord$4(value) {
8862
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8863
+ }
8864
+ function elementId(element, idKey, declared, prefix) {
8865
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8866
+ 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`);
8867
+ return id;
8868
+ }
8869
+ /**
8870
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8871
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8872
+ * result is a new body, `data` is untouched.
8873
+ */
8874
+ var REMOVE_LEAF = Symbol("remove-leaf");
8875
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8876
+ if (!isRecord$4(node)) return node;
8877
+ const segment = segments[index];
8878
+ if (segment === void 0) return node;
8879
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8880
+ const child = node[segment.key];
8881
+ const keys = [...walkedKeys, segment.key];
8882
+ const last = index === segments.length - 1;
8883
+ if (segment.each) {
8884
+ if (!Array.isArray(child)) return node;
8885
+ const prefix = keys.join(".");
8886
+ const idKey = arrayKeys[prefix];
8887
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8888
+ const mapped = child.map((element) => {
8889
+ const id = elementId(element, idKey, declared, prefix);
8890
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8891
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8892
+ });
8893
+ return {
8894
+ ...node,
8895
+ [segment.key]: mapped
8896
+ };
8897
+ }
8898
+ if (segment.map) {
8899
+ if (!isRecord$4(child)) return node;
8900
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8901
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8902
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8903
+ return [mapKey, visit({
8904
+ declared,
8905
+ concrete,
8906
+ value
8907
+ })];
8908
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8909
+ return {
8910
+ ...node,
8911
+ [segment.key]: Object.fromEntries(kept)
8912
+ };
8913
+ }
8914
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8915
+ if (last) {
8916
+ const replacement = visit({
8917
+ declared,
8918
+ concrete,
8919
+ value: child
8920
+ });
8921
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8922
+ return {
8923
+ ...node,
8924
+ [segment.key]: replacement
8925
+ };
8926
+ }
8927
+ return {
8928
+ ...node,
8929
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8930
+ };
8931
+ }
8932
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8933
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8934
+ let current = data;
8935
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8936
+ return current;
8937
+ }
8938
+ /**
8939
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8940
+ * record's refs never collide with the refs a legacy collection of the same name
8941
+ * already holds during an adoption.
8942
+ */
8943
+ function recordSecretCollection(kind) {
8944
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8945
+ }
8946
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8947
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8948
+ return buildSecretRef({
8949
+ owner,
8950
+ scope: {
8951
+ kind: "row",
8952
+ collection: recordSecretCollection(kind),
8953
+ rowId
8954
+ },
8955
+ fieldPath: concretePath
8956
+ });
8957
+ }
8958
+ /**
8959
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8960
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8961
+ * token. The form hands it to the secret field, which reveals the value by ref
8962
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8963
+ * stored secret (the records door reads a view as KEEP).
8964
+ *
8965
+ * ONE implementation for the entries service and for every addon that serves
8966
+ * its own instances (`RecordIntegrationInstances`).
8967
+ */
8968
+ function isRecord$3(value) {
8969
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8970
+ }
8971
+ function secretViewsOf(owner, kind, id, decl, data) {
8972
+ if (decl.secretPaths.length === 0) return data;
8973
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8974
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8975
+ return {
8976
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8977
+ v: 1,
8978
+ set: true
8979
+ };
8980
+ });
8981
+ return isRecord$3(shown) ? shown : data;
8982
+ }
8800
8983
  /**
8801
8984
  * The share-view scope kinds a share-link token can carry. The hub mints
8802
8985
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8869,7 +9052,8 @@ function isZodSecret(schema) {
8869
9052
  }
8870
9053
  /**
8871
9054
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8872
- * element of this array" (`connection.password`, `rows[].token`). A declared
9055
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9056
+ * map" (`applianceSecrets{}`). A declared
8873
9057
  * object or array is reported as one path and not descended into — everything
8874
9058
  * under it is secret.
8875
9059
  */
@@ -8880,6 +9064,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8880
9064
  const def = defOf(layer);
8881
9065
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8882
9066
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9067
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
8883
9068
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8884
9069
  else if (def.type === "intersection") {
8885
9070
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9019,7 +9204,7 @@ function defineRecordKinds(specs) {
9019
9204
  retention: spec.retention,
9020
9205
  deviceRemoval: spec.deviceRemoval,
9021
9206
  maxDataBytes: spec.maxDataBytes,
9022
- secretPaths: [...secretPaths],
9207
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9023
9208
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9024
9209
  transactFamily: spec.transactFamily,
9025
9210
  entry: spec.entry === void 0 ? void 0 : {
@@ -9064,6 +9249,7 @@ function defineEntryKind(name, spec) {
9064
9249
  ...spec.database === void 0 ? {} : { database: spec.database },
9065
9250
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9066
9251
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9252
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9067
9253
  entry: {
9068
9254
  title: spec.title,
9069
9255
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11739,7 +11925,7 @@ function userError(code, params, englishFallback) {
11739
11925
  return new CamstackError(code, params, englishFallback);
11740
11926
  }
11741
11927
  //#endregion
11742
- //#region ../types/dist/composition-BodkvOV0.mjs
11928
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11743
11929
  /**
11744
11930
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11745
11931
  *
@@ -12502,38 +12688,6 @@ function validateExpressionSource(src) {
12502
12688
  return null;
12503
12689
  }
12504
12690
  /**
12505
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12506
- *
12507
- * Standby of the control board is under 5 W; a motor or a heater is far
12508
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12509
- * a cycle, so running starts only after 30 s above the line.
12510
- *
12511
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12512
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12513
- * report "finished" in the middle of the program, so the stop dwell is
12514
- * 10 minutes on every type. The number is stored per type so one machine
12515
- * can move later without moving the others.
12516
- */
12517
- var APPLIANCE_TEMPLATE_ID = "appliance";
12518
- var APPLIANCE_KINDS = [
12519
- "washer",
12520
- "dryer",
12521
- "dishwasher"
12522
- ];
12523
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12524
- /**
12525
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12526
- *
12527
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12528
- * (D967), so the heater switches exactly as the plug does. Running is the
12529
- * plug being on; when a power reading is also named, running is the draw
12530
- * staying above the line instead (a plug left on with the stove's own
12531
- * thermostat open is not heating). 50 W is far above any standby and below
12532
- * the smallest radiator.
12533
- */
12534
- var HEATER_TEMPLATE_ID = "heater";
12535
- DeviceType.Heater;
12536
- /**
12537
12691
  * Statistics and integrals over a history read (D944).
12538
12692
  *
12539
12693
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12677,34 +12831,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12677
12831
  /** Actuator groups only. Readings leave this unset. */
12678
12832
  active: ActiveCriterionSchema.optional()
12679
12833
  }),
12680
- object({
12681
- kind: literal("appliance"),
12682
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12683
- appliance: _enum(APPLIANCE_KINDS),
12684
- thresholdWatts: number().positive(),
12685
- startDwellMs: number().int().nonnegative(),
12686
- stopDwellMs: number().int().nonnegative(),
12687
- member: HelperMemberSchema,
12688
- /**
12689
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12690
- * composition carries a `power-source` feature, so the hub can cut and restore
12691
- * that plug (power-source link of a NEW-target composition).
12692
- */
12693
- powerSwitch: HelperMemberSchema.optional(),
12694
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12695
- offSeconds: number().optional()
12696
- }),
12697
- object({
12698
- kind: literal("heater"),
12699
- templateId: literal(HEATER_TEMPLATE_ID),
12700
- /** The plug, read and commanded at `switch.on`. */
12701
- plug: HelperMemberSchema,
12702
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12703
- meter: HelperMemberSchema.optional(),
12704
- thresholdWatts: number().positive(),
12705
- startDwellMs: number().int().nonnegative(),
12706
- stopDwellMs: number().int().nonnegative()
12707
- }),
12708
12834
  object({
12709
12835
  kind: literal("statistic"),
12710
12836
  stat: _enum(STATISTIC_STATS),
@@ -12928,7 +13054,7 @@ var CompositionSchema = object({
12928
13054
  kind: literal("existing"),
12929
13055
  device: CompositionSourceRefSchema
12930
13056
  })]),
12931
- features: array(CompositionFeatureSchema).min(1).max(16),
13057
+ features: array(CompositionFeatureSchema).min(0).max(16),
12932
13058
  /**
12933
13059
  * Present when the block is a helper template. Absent on a free
12934
13060
  * composition (the room example, device 4603). The editor reads this,
@@ -13263,6 +13389,205 @@ function buildCapturingReply(envelope) {
13263
13389
  };
13264
13390
  return wrapper;
13265
13391
  }
13392
+ /**
13393
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13394
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13395
+ *
13396
+ * Every instance is one record of the addon's own kind, keyed by the
13397
+ * `integrations` registry id. The records door does the secret work: a field the
13398
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13399
+ * deleting the row deletes the secret, and one row's value is never another's.
13400
+ *
13401
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13402
+ * token or the view the form was given KEEP the stored secret;
13403
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13404
+ */
13405
+ /** The most instances one addon lists (an integrations page, not a table). */
13406
+ var INSTANCES_LIST_MAX = 1e3;
13407
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13408
+ function topLevelSecretKeys(paths) {
13409
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13410
+ }
13411
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13412
+ function secretMapKeys(paths) {
13413
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13414
+ }
13415
+ /**
13416
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13417
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13418
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13419
+ */
13420
+ function secretMapPatch(value) {
13421
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13422
+ const out = {};
13423
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13424
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13425
+ return Object.keys(out).length > 0 ? out : null;
13426
+ }
13427
+ /** Does the kind's body schema carry an `enabled` field? */
13428
+ function declaresEnabled(kind) {
13429
+ const json = toJSONSchema(kind.schema, {
13430
+ unrepresentable: "any",
13431
+ io: "input"
13432
+ });
13433
+ const properties = Reflect.get(json, "properties");
13434
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13435
+ }
13436
+ var RecordIntegrationInstances = class {
13437
+ records;
13438
+ kind;
13439
+ hooks;
13440
+ handle;
13441
+ secretKeys;
13442
+ secretMaps;
13443
+ nameField;
13444
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13445
+ registryOwnsEnabled;
13446
+ constructor(records, kind, hooks) {
13447
+ this.records = records;
13448
+ this.kind = kind;
13449
+ this.hooks = hooks;
13450
+ this.handle = records.kind(kind);
13451
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13452
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13453
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13454
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13455
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
13456
+ }
13457
+ /**
13458
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13459
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13460
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13461
+ */
13462
+ async readResolved(id) {
13463
+ try {
13464
+ const record = await this.handle.getResolved(id);
13465
+ return record === null ? null : {
13466
+ id,
13467
+ body: record.data,
13468
+ enabled: record.data.enabled !== false,
13469
+ secrets: "resolved"
13470
+ };
13471
+ } catch (err) {
13472
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13473
+ const stored = await this.handle.get(id);
13474
+ if (stored === null) return null;
13475
+ return {
13476
+ id,
13477
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13478
+ enabled: stored.data.enabled !== false,
13479
+ secrets: "unavailable"
13480
+ };
13481
+ }
13482
+ }
13483
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13484
+ async getResolved(id) {
13485
+ return (await this.readResolved(id))?.body ?? null;
13486
+ }
13487
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13488
+ async listResolved() {
13489
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13490
+ const out = [];
13491
+ for (const record of page.records) {
13492
+ const read = await this.readResolved(record.id);
13493
+ if (read !== null) out.push(read);
13494
+ }
13495
+ return out;
13496
+ }
13497
+ viewOf(record) {
13498
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13499
+ const rawName = record.data[this.nameField];
13500
+ const kind = this.hooks.kindOf?.(record.data);
13501
+ const health = this.hooks.health?.(record.data, record.id);
13502
+ return {
13503
+ id: record.id,
13504
+ name: typeof rawName === "string" ? rawName : record.id,
13505
+ configured: this.hooks.configured(record.data),
13506
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13507
+ formValues: shown,
13508
+ ...kind !== void 0 ? { kind } : {},
13509
+ ...health?.status !== void 0 ? { status: health.status } : {},
13510
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13511
+ };
13512
+ }
13513
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13514
+ ...this.hooks.description,
13515
+ mintsId: true
13516
+ };
13517
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13518
+ listInstances = async () => {
13519
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13520
+ };
13521
+ getInstance = async ({ id }) => {
13522
+ const record = await this.handle.get(id);
13523
+ return record === null ? null : this.viewOf(record);
13524
+ };
13525
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13526
+ createInstance = async ({ id, name, kind }) => {
13527
+ const minted = id ?? this.hooks.mintId?.({
13528
+ name,
13529
+ ...kind !== void 0 ? { kind } : {}
13530
+ });
13531
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13532
+ await this.handle.put({
13533
+ id: minted,
13534
+ data: this.hooks.initialBody(name, kind)
13535
+ }, { ifRev: 0 });
13536
+ await this.hooks.onChanged?.(minted, "created");
13537
+ return { id: minted };
13538
+ };
13539
+ updateInstance = async ({ id, name, values }) => {
13540
+ const patch = {};
13541
+ for (const [key, value] of Object.entries(values)) {
13542
+ if (value === void 0) continue;
13543
+ if (this.secretMaps.has(key)) {
13544
+ const entries = secretMapPatch(value);
13545
+ if (entries !== null) patch[key] = entries;
13546
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13547
+ else if (isSecretClear(value)) patch[key] = null;
13548
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13549
+ }
13550
+ if (name !== void 0) patch[this.nameField] = name;
13551
+ await this.handle.patch(id, patch);
13552
+ await this.hooks.onChanged?.(id, "updated");
13553
+ return { ok: true };
13554
+ };
13555
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13556
+ setInstanceEnabled = async ({ id, enabled }) => {
13557
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13558
+ await this.handle.patch(id, { enabled });
13559
+ await this.hooks.onChanged?.(id, "enabled");
13560
+ return { ok: true };
13561
+ };
13562
+ /** The row and its keychain refs go together (the records door). */
13563
+ deleteInstance = async ({ id }) => {
13564
+ await this.handle.delete(id);
13565
+ await this.hooks.onChanged?.(id, "deleted");
13566
+ return { ok: true };
13567
+ };
13568
+ testInstance = async (input) => {
13569
+ if (this.hooks.test === void 0) return {
13570
+ success: false,
13571
+ status: "unsupported",
13572
+ error: "This integration declares no connection test"
13573
+ };
13574
+ return this.hooks.test(input);
13575
+ };
13576
+ /** The flat provider object to hand to `registerProvider`. */
13577
+ toProvider() {
13578
+ return {
13579
+ describe: this.describe,
13580
+ instanceFormSchema: this.instanceFormSchema,
13581
+ listInstances: this.listInstances,
13582
+ getInstance: this.getInstance,
13583
+ createInstance: this.createInstance,
13584
+ updateInstance: this.updateInstance,
13585
+ setInstanceEnabled: this.setInstanceEnabled,
13586
+ deleteInstance: this.deleteInstance,
13587
+ testInstance: this.testInstance
13588
+ };
13589
+ }
13590
+ };
13266
13591
  var EncodeProfileSchema = object({
13267
13592
  video: object({
13268
13593
  codec: _enum([
@@ -48716,49 +49041,6 @@ function actuator(input) {
48716
49041
  };
48717
49042
  }
48718
49043
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
48719
- var PowerSourceRefusalReasonSchema = _enum([
48720
- "no-power-source",
48721
- "switch-missing",
48722
- "switch-offline",
48723
- "switch-unknown",
48724
- "restart-in-progress",
48725
- "switch-call-failed"
48726
- ]);
48727
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
48728
- accepted: literal(false),
48729
- reason: PowerSourceRefusalReasonSchema,
48730
- /** One line for the operator, naming the switch. */
48731
- message: string()
48732
- })]);
48733
- var PowerSourceInfoSchema = object({
48734
- /** False when the device has no power source linked; the rest is then empty. */
48735
- linked: boolean(),
48736
- switchDeviceId: number().nullable(),
48737
- switchName: string().nullable(),
48738
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
48739
- switchOn: boolean().nullable(),
48740
- switchOnline: boolean().nullable(),
48741
- restartInProgress: boolean(),
48742
- offSeconds: number().int()
48743
- });
48744
- object({
48745
- /** Null while the switch has not reported: unknown is never `false` (D393). */
48746
- switchOn: boolean().nullable(),
48747
- offSeconds: number().int(),
48748
- lastFetchedAt: number()
48749
- });
48750
- 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;
48751
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
48752
- systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48753
- kind: "mutation",
48754
- auth: "admin"
48755
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48756
- kind: "mutation",
48757
- auth: "admin"
48758
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48759
- kind: "mutation",
48760
- auth: "admin"
48761
- });
48762
49044
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
48763
49045
  DeviceType.Camera;
48764
49046
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -48786,6 +49068,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
48786
49068
  }).optional()
48787
49069
  })) });
48788
49070
  /**
49071
+ * A saved Customize assignment (D936). The schema is the type: callers use
49072
+ * `z.infer`, they do not restate the shape.
49073
+ */
49074
+ var OverrideSourceSchema = object({
49075
+ addonId: string().min(1),
49076
+ stableId: string().min(1),
49077
+ cap: string().min(1),
49078
+ fieldPath: string().min(1)
49079
+ });
49080
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
49081
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
49082
+ var ThresholdTransformDraftSchema = object({
49083
+ kind: literal("threshold"),
49084
+ comparison: _enum(["gte", "lte"]),
49085
+ threshold: number(),
49086
+ startDwellMs: number().nonnegative(),
49087
+ stopDwellMs: number().nonnegative()
49088
+ });
49089
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
49090
+ IdentityTransformDraftSchema,
49091
+ InvertTransformDraftSchema,
49092
+ ThresholdTransformDraftSchema,
49093
+ object({
49094
+ kind: literal("linear"),
49095
+ scale: number(),
49096
+ offset: number(),
49097
+ clampMin: number().optional(),
49098
+ clampMax: number().optional()
49099
+ }),
49100
+ object({
49101
+ kind: literal("enum-map"),
49102
+ mapping: record(string(), string().nullable())
49103
+ }),
49104
+ object({ kind: literal("number") })
49105
+ ]);
49106
+ var ScalarOverrideDraftSchema = object({
49107
+ kind: literal("scalar"),
49108
+ cap: string().min(1),
49109
+ targetId: string().min(1),
49110
+ source: OverrideSourceSchema,
49111
+ transform: CustomizeTransformDraftSchema
49112
+ });
49113
+ var KeyedArrayOverrideDraftSchema = object({
49114
+ kind: literal("keyedArray"),
49115
+ cap: string().min(1),
49116
+ targetId: string().min(1),
49117
+ key: string().min(1),
49118
+ label: string().min(1),
49119
+ fieldPath: string().min(1),
49120
+ source: OverrideSourceSchema,
49121
+ transform: CustomizeTransformDraftSchema
49122
+ });
49123
+ var PulseOverrideDraftSchema = object({
49124
+ kind: literal("pulse"),
49125
+ cap: string().min(1),
49126
+ targetId: string().min(1),
49127
+ enabled: boolean(),
49128
+ debounceMs: number().nonnegative(),
49129
+ edges: array(_enum([
49130
+ "rising",
49131
+ "button",
49132
+ "native-event"
49133
+ ])).min(1).optional(),
49134
+ sources: array(OverrideSourceSchema).min(1)
49135
+ });
49136
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
49137
+ "in-addition",
49138
+ "confirm",
49139
+ "replace"
49140
+ ]).optional() });
49141
+ discriminatedUnion("kind", [
49142
+ ScalarOverrideDraftSchema,
49143
+ KeyedArrayOverrideDraftSchema,
49144
+ PulseOverrideDraftSchema,
49145
+ object({
49146
+ kind: literal("span"),
49147
+ cap: string().min(1),
49148
+ targetId: string().min(1),
49149
+ timeoutMs: number().nonnegative(),
49150
+ cooldownMs: number().nonnegative().optional(),
49151
+ sources: array(SpanSourceDraftSchema)
49152
+ })
49153
+ ]);
49154
+ object({
49155
+ targetId: string().min(1),
49156
+ configured: boolean(),
49157
+ source: _enum(["native", "assigned"]),
49158
+ state: _enum([
49159
+ "active",
49160
+ "stale",
49161
+ "released",
49162
+ "unavailable"
49163
+ ]),
49164
+ reason: string().nullable()
49165
+ });
49166
+ /**
49167
+ * What a running-threshold proposal says (planning#405). The schema is the
49168
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
49169
+ *
49170
+ * A proposal is read-only advice: it carries the numbers it was derived from
49171
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
49172
+ * WHY with a typed reason and the catalog defaults, never an invented number.
49173
+ */
49174
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
49175
+ var RunningProposalConfidenceSchema = _enum([
49176
+ "high",
49177
+ "medium",
49178
+ "low"
49179
+ ]);
49180
+ var RunningProposalInsufficientReasonSchema = _enum([
49181
+ "no-history",
49182
+ "span-too-short",
49183
+ "truncated",
49184
+ "no-idle-floor",
49185
+ "no-runs",
49186
+ "no-separation"
49187
+ ]);
49188
+ var RunningProposalReplaySchema = object({
49189
+ /** Intervals the real dwell expression held true over the window. */
49190
+ intervals: number().int().nonnegative(),
49191
+ /** Intervals outside every run, or a second interval inside one. */
49192
+ extra: number().int().nonnegative(),
49193
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
49194
+ missed: number().int().nonnegative()
49195
+ });
49196
+ var RunningProposalEvidenceSchema = object({
49197
+ tier: RunningProposalTierSchema,
49198
+ spanMs: number().nonnegative(),
49199
+ from: number(),
49200
+ to: number(),
49201
+ points: number().int().nonnegative(),
49202
+ runsFound: number().int().nonnegative(),
49203
+ pausesFound: number().int().nonnegative(),
49204
+ longestPauseMs: number().nonnegative(),
49205
+ idleFloorW: number(),
49206
+ noiseCeilingW: number(),
49207
+ plateauW: number(),
49208
+ replay: RunningProposalReplaySchema
49209
+ });
49210
+ var RunningProposalInsufficientEvidenceSchema = object({
49211
+ spanMs: number().nonnegative(),
49212
+ points: number().int().nonnegative(),
49213
+ runsFound: number().int().nonnegative()
49214
+ });
49215
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
49216
+ state: literal("proposal"),
49217
+ transform: ThresholdTransformDraftSchema,
49218
+ evidence: RunningProposalEvidenceSchema,
49219
+ confidence: RunningProposalConfidenceSchema,
49220
+ /** Why the confidence is not higher; empty when nothing held it back. */
49221
+ notes: array(string())
49222
+ }), object({
49223
+ state: literal("insufficient"),
49224
+ reason: RunningProposalInsufficientReasonSchema,
49225
+ evidence: RunningProposalInsufficientEvidenceSchema,
49226
+ /** The catalog defaults, offered as what stands when nothing better is known. */
49227
+ fallback: ThresholdTransformDraftSchema
49228
+ })]);
49229
+ /**
48789
49230
  * The report of a one-shot "move this owner's plaintext secrets into the
48790
49231
  * keychain" run (D878). COUNTS only - never a value, never a ref.
48791
49232
  *
@@ -49075,6 +49516,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49075
49516
  kind: "mutation",
49076
49517
  auth: "admin",
49077
49518
  access: "view"
49519
+ }), method(object({
49520
+ deviceId: number().int(),
49521
+ cap: string().min(1),
49522
+ fieldPath: string().min(1),
49523
+ deviceType: _enum(DeviceType).optional()
49524
+ }), RunningThresholdProposalSchema, {
49525
+ auth: "admin",
49526
+ access: "view"
49078
49527
  }), method(object({
49079
49528
  blockId: string().min(1),
49080
49529
  trace: AutomationRunTraceSchema.optional(),
@@ -49883,7 +50332,27 @@ method(object({
49883
50332
  * exposes only the COMMON surface: link state, supported device
49884
50333
  * kinds (so the UI can filter the device picker), and per-device
49885
50334
  * expose/unexpose.
49886
- */
50335
+ *
50336
+ * INSTANCES. One export addon can serve several independent targets (a
50337
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
50338
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
50339
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
50340
+ * default instance's); present = that instance; an id the provider does not
50341
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
50342
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
50343
+ * change always names the target it changes. A single-instance provider
50344
+ * accepts an absent id on a read or its own id, and refuses any other.
50345
+ */
50346
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
50347
+ var InstanceIdSchema$1 = string().min(1);
50348
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
50349
+ /** One independent export target of an addon. */
50350
+ var ExportInstanceSchema = object({
50351
+ instanceId: InstanceIdSchema$1,
50352
+ name: string(),
50353
+ enabled: boolean(),
50354
+ kind: string().optional()
50355
+ });
49887
50356
  var LinkStateSchema = _enum([
49888
50357
  "unlinked",
49889
50358
  "linked",
@@ -49934,10 +50403,14 @@ var ExposedDeviceSchema = object({
49934
50403
  capabilities: array(string()).optional()
49935
50404
  });
49936
50405
  var ExposeInputSchema = object({
50406
+ instanceId: InstanceIdSchema$1,
49937
50407
  deviceId: string(),
49938
50408
  capabilities: array(string()).optional()
49939
50409
  });
49940
- var UnexposeInputSchema = object({ deviceId: string() });
50410
+ var UnexposeInputSchema = object({
50411
+ instanceId: InstanceIdSchema$1,
50412
+ deviceId: string()
50413
+ });
49941
50414
  /**
49942
50415
  * What an exporter may expose besides devices (D968). An action and a
49943
50416
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -49966,15 +50439,21 @@ var ExposedEntitySchema = object({
49966
50439
  entity: ExportEntityRefSchema,
49967
50440
  exposedAs: string().optional()
49968
50441
  });
49969
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
49970
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
49971
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50442
+ var ExposeEntityInputSchema = object({
50443
+ instanceId: InstanceIdSchema$1,
50444
+ entity: ExportEntityRefSchema
50445
+ });
50446
+ var UnexposeEntityInputSchema = object({
50447
+ instanceId: InstanceIdSchema$1,
50448
+ entity: ExportEntityRefSchema
50449
+ });
50450
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
49972
50451
  kind: "mutation",
49973
50452
  auth: "admin"
49974
50453
  }), method(UnexposeInputSchema, _void(), {
49975
50454
  kind: "mutation",
49976
50455
  auth: "admin"
49977
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50456
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
49978
50457
  kind: "mutation",
49979
50458
  auth: "admin"
49980
50459
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -53175,15 +53654,15 @@ var FanPresetCaptureResultSchema = object({
53175
53654
  items: array(FanCaptureItemSchema),
53176
53655
  omitted: array(FanCaptureOmissionSchema)
53177
53656
  });
53178
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
53657
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
53179
53658
  DeviceType.Fan;
53180
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53659
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53181
53660
  kind: "mutation",
53182
53661
  auth: "admin"
53183
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53662
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53184
53663
  kind: "mutation",
53185
53664
  auth: "protected"
53186
- }), method(deviceInput$1.extend({
53665
+ }), method(deviceInput$2.extend({
53187
53666
  fields: array(FanCaptureFieldSchema).optional(),
53188
53667
  steps: array(ActionStepSchema).max(20).default([])
53189
53668
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -53707,13 +54186,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53707
54186
  */
53708
54187
  var ConfigSchemaPassthrough$3 = custom();
53709
54188
  var IntegrationInstanceIdSchema = string().min(1);
54189
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54190
+ /**
54191
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54192
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54193
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
54194
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
54195
+ * instance id.
54196
+ */
54197
+ var IntegrationInstanceKindSchema = object({
54198
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
54199
+ id: string().min(1),
54200
+ label: string().min(1),
54201
+ /** The kind signs in through a server-held interactive session. */
54202
+ signIn: object({
54203
+ basePath: string().min(1),
54204
+ methods: array(_enum(["device-code", "proxy"])).optional()
54205
+ }).optional(),
54206
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
54207
+ connectPath: string().min(1).optional(),
54208
+ /** Relative hub path to disconnect that account. */
54209
+ disconnectPath: string().min(1).optional()
54210
+ });
53710
54211
  var IntegrationInstanceDescriptionSchema = object({
54212
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54213
+ role: _enum([
54214
+ "devices",
54215
+ "services",
54216
+ "both"
54217
+ ]),
54218
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54219
+ discovery: object({
54220
+ mode: _enum([
54221
+ "none",
54222
+ "lan",
54223
+ "account"
54224
+ ]),
54225
+ scopedToInstance: boolean()
54226
+ }).optional(),
53711
54227
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
53712
- instanceMode: _enum(["single", "multiple"]),
54228
+ instanceMode: IntegrationInstanceModeSchema,
53713
54229
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
53714
54230
  kind: string(),
53715
54231
  /** True when `testInstance` really probes the remote. */
53716
- canTest: boolean()
54232
+ canTest: boolean(),
54233
+ /**
54234
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
54235
+ * Add flow asks which first and sends it as `createInstance.kind`.
54236
+ */
54237
+ kinds: array(IntegrationInstanceKindSchema).optional(),
54238
+ /**
54239
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
54240
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
54241
+ */
54242
+ allowRestoreId: boolean().optional(),
54243
+ /**
54244
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
54245
+ * send an id (the UI then mints an opaque one).
54246
+ */
54247
+ mintsId: boolean().optional()
53717
54248
  });
53718
54249
  var IntegrationInstanceStatusSchema = object({
53719
54250
  state: _enum([
@@ -53722,7 +54253,9 @@ var IntegrationInstanceStatusSchema = object({
53722
54253
  "error",
53723
54254
  "unknown"
53724
54255
  ]),
53725
- detail: string().optional()
54256
+ detail: string().optional(),
54257
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
54258
+ label: string().optional()
53726
54259
  });
53727
54260
  var IntegrationInstanceSchema = object({
53728
54261
  id: IntegrationInstanceIdSchema,
@@ -53730,13 +54263,18 @@ var IntegrationInstanceSchema = object({
53730
54263
  /** The addon's own rule (an URL is present, a client id is present...). */
53731
54264
  configured: boolean(),
53732
54265
  /**
53733
- * The instance's own on/off switch (D996): always present, derived from the record body
53734
- * (`enabled !== false`, so a row written before the field existed is on).
54266
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
54267
+ * so a row written before the field existed is on). Absent when the `integrations` registry
54268
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
53735
54269
  */
53736
- enabled: boolean(),
54270
+ enabled: boolean().optional(),
54271
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
54272
+ kind: string().optional(),
53737
54273
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
53738
54274
  formValues: record(string(), unknown()),
53739
- status: IntegrationInstanceStatusSchema.optional()
54275
+ status: IntegrationInstanceStatusSchema.optional(),
54276
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
54277
+ lastCheckedAt: number().optional()
53740
54278
  });
53741
54279
  var IntegrationInstancePatchSchema = object({
53742
54280
  id: IntegrationInstanceIdSchema,
@@ -53744,31 +54282,57 @@ var IntegrationInstancePatchSchema = object({
53744
54282
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
53745
54283
  values: record(string(), unknown())
53746
54284
  });
53747
- 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({
53748
- id: IntegrationInstanceIdSchema,
53749
- name: string().min(1)
53750
- }), object({ id: string() }), {
53751
- kind: "mutation",
53752
- auth: "admin"
53753
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
53754
- kind: "mutation",
53755
- auth: "admin"
53756
- }), method(object({
53757
- id: IntegrationInstanceIdSchema,
53758
- enabled: boolean()
53759
- }), object({ ok: literal(true) }), {
53760
- kind: "mutation",
53761
- auth: "admin"
53762
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
53763
- kind: "mutation",
53764
- auth: "admin"
53765
- }), optionalMethod(object({
53766
- id: IntegrationInstanceIdSchema.nullable(),
53767
- values: record(string(), unknown())
53768
- }), TestConnectionResultSchema$1, {
53769
- kind: "mutation",
53770
- auth: "admin"
53771
- });
54285
+ var integrationInstancesCapability = {
54286
+ name: "integration-instances",
54287
+ scope: "system",
54288
+ mode: "collection",
54289
+ methods: {
54290
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
54291
+ /** The form; `id: null` is the create form. */
54292
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54293
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54294
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54295
+ /**
54296
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
54297
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
54298
+ */
54299
+ createInstance: method(object({
54300
+ id: IntegrationInstanceIdSchema.optional(),
54301
+ name: string().min(1),
54302
+ kind: string().min(1).optional()
54303
+ }), object({ id: string() }), {
54304
+ kind: "mutation",
54305
+ auth: "admin"
54306
+ }),
54307
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
54308
+ kind: "mutation",
54309
+ auth: "admin"
54310
+ }),
54311
+ /**
54312
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
54313
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
54314
+ */
54315
+ setInstanceEnabled: method(object({
54316
+ id: IntegrationInstanceIdSchema,
54317
+ enabled: boolean()
54318
+ }), object({ ok: literal(true) }), {
54319
+ kind: "mutation",
54320
+ auth: "admin"
54321
+ }),
54322
+ /** Delete the record and its keychain refs together. */
54323
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
54324
+ kind: "mutation",
54325
+ auth: "admin"
54326
+ }),
54327
+ testInstance: optionalMethod(object({
54328
+ id: IntegrationInstanceIdSchema.nullable(),
54329
+ values: record(string(), unknown())
54330
+ }), TestConnectionResultSchema$1, {
54331
+ kind: "mutation",
54332
+ auth: "admin"
54333
+ })
54334
+ }
54335
+ };
53772
54336
  /**
53773
54337
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
53774
54338
  * session (server-initiated offer pattern, identical to the main
@@ -55567,6 +56131,11 @@ var LogEntrySchema = object({
55567
56131
  * flight, and not asking for the next slice ends the scan.
55568
56132
  */
55569
56133
  var LogReadQuerySchema = object({
56134
+ /**
56135
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
56136
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
56137
+ */
56138
+ instanceId: string().min(1).max(200).optional(),
55570
56139
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
55571
56140
  requestId: string().min(1).max(64).optional(),
55572
56141
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -55629,6 +56198,8 @@ var LogReadResultSchema = object({
55629
56198
  /** True when `cancelRead` ended this slice early. */
55630
56199
  cancelled: boolean().optional()
55631
56200
  });
56201
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
56202
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
55632
56203
  var LogReadCapabilitiesSchema = object({
55633
56204
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
55634
56205
  readable: boolean(),
@@ -55647,7 +56218,7 @@ var LogReadCapabilitiesSchema = object({
55647
56218
  /** How far back the store reaches, when the provider knows (days). */
55648
56219
  retentionDays: number().nullable()
55649
56220
  });
55650
- 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() }), {
56221
+ 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() }), {
55651
56222
  kind: "mutation",
55652
56223
  auth: "admin"
55653
56224
  });
@@ -67219,6 +67790,49 @@ object({
67219
67790
  DeviceType.PetFeeder,
67220
67791
  DeviceType.Heater
67221
67792
  ].filter((type) => type !== DeviceType.Sensor);
67793
+ var PowerSourceRefusalReasonSchema = _enum([
67794
+ "no-power-source",
67795
+ "switch-missing",
67796
+ "switch-offline",
67797
+ "switch-unknown",
67798
+ "restart-in-progress",
67799
+ "switch-call-failed"
67800
+ ]);
67801
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
67802
+ accepted: literal(false),
67803
+ reason: PowerSourceRefusalReasonSchema,
67804
+ /** One line for the operator, naming the switch. */
67805
+ message: string()
67806
+ })]);
67807
+ var PowerSourceInfoSchema = object({
67808
+ /** False when the device has no power source linked; the rest is then empty. */
67809
+ linked: boolean(),
67810
+ switchDeviceId: number().nullable(),
67811
+ switchName: string().nullable(),
67812
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
67813
+ switchOn: boolean().nullable(),
67814
+ switchOnline: boolean().nullable(),
67815
+ restartInProgress: boolean(),
67816
+ offSeconds: number().int()
67817
+ });
67818
+ object({
67819
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
67820
+ switchOn: boolean().nullable(),
67821
+ offSeconds: number().int(),
67822
+ lastFetchedAt: number()
67823
+ });
67824
+ 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;
67825
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
67826
+ systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67827
+ kind: "mutation",
67828
+ auth: "admin"
67829
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67830
+ kind: "mutation",
67831
+ auth: "admin"
67832
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67833
+ kind: "mutation",
67834
+ auth: "admin"
67835
+ });
67222
67836
  /**
67223
67837
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
67224
67838
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -75285,6 +75899,25 @@ function buildDescriptor(capName, kind) {
75285
75899
  * contributes one entry per bound cap present in this map.
75286
75900
  */
75287
75901
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
75902
+ /**
75903
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
75904
+ * shared by the admin UI and the viewer. A role is data on the wire; each
75905
+ * client resolves its name from `label` (English and Italian side by side) and
75906
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
75907
+ * server sends an id, a client maps it to its own icon set.
75908
+ *
75909
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
75910
+ * pictogram): a client that can draw text shows it, one that cannot falls back
75911
+ * to `icon`.
75912
+ */
75913
+ function digit(n) {
75914
+ return {
75915
+ label: t(n, n),
75916
+ icon: "hash",
75917
+ legend: n
75918
+ };
75919
+ }
75920
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
75288
75921
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
75289
75922
  var btn = (idSuffix, row, col, init) => ({
75290
75923
  idSuffix,
@@ -75412,6 +76045,34 @@ discriminatedUnion("kind", [object({
75412
76045
  kind: literal("pronto"),
75413
76046
  code: string()
75414
76047
  })]);
76048
+ /**
76049
+ * Propose a running threshold from a device's own power history (planning#405).
76050
+ *
76051
+ * Pure and deterministic: the same series gives the same answer. The series is
76052
+ * a HELD-VALUE step series — a point's value stands until the next point — which
76053
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
76054
+ * count of samples.
76055
+ *
76056
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
76057
+ * near F. A device that is not near one level most of the time has no floor.
76058
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
76059
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
76060
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
76061
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
76062
+ * grid a person would type; stop dwell covers the longest pause; start dwell
76063
+ * covers the longest standby blip above T.
76064
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
76065
+ * run must hold exactly one true interval and nothing may flap outside.
76066
+ */
76067
+ var SECOND_MS = 1e3;
76068
+ var MINUTE_MS$1 = 60 * SECOND_MS;
76069
+ 24 * (60 * MINUTE_MS$1);
76070
+ 10 * MINUTE_MS$1;
76071
+ 30 * SECOND_MS;
76072
+ 60 * SECOND_MS;
76073
+ 20 * MINUTE_MS$1;
76074
+ 10 * SECOND_MS;
76075
+ 10 * SECOND_MS;
75415
76076
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75416
76077
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
75417
76078
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -76953,6 +77614,13 @@ Object.freeze({
76953
77614
  access: "create",
76954
77615
  role: "admin"
76955
77616
  },
77617
+ "coreBlocks.proposeRunningThreshold": {
77618
+ capName: "core-blocks",
77619
+ capScope: "system",
77620
+ addonId: null,
77621
+ access: "view",
77622
+ role: "admin"
77623
+ },
76956
77624
  "coreBlocks.reportAutomationRun": {
76957
77625
  capName: "core-blocks",
76958
77626
  capScope: "system",
@@ -77464,6 +78132,13 @@ Object.freeze({
77464
78132
  access: "view",
77465
78133
  role: "admin"
77466
78134
  },
78135
+ "deviceExport.listInstances": {
78136
+ capName: "device-export",
78137
+ capScope: "system",
78138
+ addonId: null,
78139
+ access: "view",
78140
+ role: "admin"
78141
+ },
77467
78142
  "deviceExport.listSupportedDeviceKinds": {
77468
78143
  capName: "device-export",
77469
78144
  capScope: "system",
@@ -86411,6 +87086,11 @@ Object.freeze({
86411
87086
  form: "single",
86412
87087
  optional: false
86413
87088
  }],
87089
+ "coreBlocks.proposeRunningThreshold": [{
87090
+ name: "deviceId",
87091
+ form: "single",
87092
+ optional: false
87093
+ }],
86414
87094
  "cover.close": [{
86415
87095
  name: "deviceId",
86416
87096
  form: "single",
@@ -89074,99 +89754,6 @@ Object.freeze({
89074
89754
  "network-access": "ingress",
89075
89755
  "smtp-provider": "email"
89076
89756
  });
89077
- /**
89078
- * A saved Customize assignment (D936). The schema is the type: callers use
89079
- * `z.infer`, they do not restate the shape.
89080
- */
89081
- var OverrideSourceSchema = object({
89082
- addonId: string().min(1),
89083
- stableId: string().min(1),
89084
- cap: string().min(1),
89085
- fieldPath: string().min(1)
89086
- });
89087
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
89088
- object({ kind: literal("identity") }),
89089
- object({ kind: literal("invert") }),
89090
- object({
89091
- kind: literal("threshold"),
89092
- comparison: _enum(["gte", "lte"]),
89093
- threshold: number(),
89094
- startDwellMs: number().nonnegative(),
89095
- stopDwellMs: number().nonnegative()
89096
- }),
89097
- object({
89098
- kind: literal("linear"),
89099
- scale: number(),
89100
- offset: number(),
89101
- clampMin: number().optional(),
89102
- clampMax: number().optional()
89103
- }),
89104
- object({
89105
- kind: literal("enum-map"),
89106
- mapping: record(string(), string().nullable())
89107
- }),
89108
- object({ kind: literal("number") })
89109
- ]);
89110
- var ScalarOverrideDraftSchema = object({
89111
- kind: literal("scalar"),
89112
- cap: string().min(1),
89113
- targetId: string().min(1),
89114
- source: OverrideSourceSchema,
89115
- transform: CustomizeTransformDraftSchema
89116
- });
89117
- var KeyedArrayOverrideDraftSchema = object({
89118
- kind: literal("keyedArray"),
89119
- cap: string().min(1),
89120
- targetId: string().min(1),
89121
- key: string().min(1),
89122
- label: string().min(1),
89123
- fieldPath: string().min(1),
89124
- source: OverrideSourceSchema,
89125
- transform: CustomizeTransformDraftSchema
89126
- });
89127
- var PulseOverrideDraftSchema = object({
89128
- kind: literal("pulse"),
89129
- cap: string().min(1),
89130
- targetId: string().min(1),
89131
- enabled: boolean(),
89132
- debounceMs: number().nonnegative(),
89133
- edges: array(_enum([
89134
- "rising",
89135
- "button",
89136
- "native-event"
89137
- ])).min(1).optional(),
89138
- sources: array(OverrideSourceSchema).min(1)
89139
- });
89140
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
89141
- "in-addition",
89142
- "confirm",
89143
- "replace"
89144
- ]).optional() });
89145
- discriminatedUnion("kind", [
89146
- ScalarOverrideDraftSchema,
89147
- KeyedArrayOverrideDraftSchema,
89148
- PulseOverrideDraftSchema,
89149
- object({
89150
- kind: literal("span"),
89151
- cap: string().min(1),
89152
- targetId: string().min(1),
89153
- timeoutMs: number().nonnegative(),
89154
- cooldownMs: number().nonnegative().optional(),
89155
- sources: array(SpanSourceDraftSchema)
89156
- })
89157
- ]);
89158
- object({
89159
- targetId: string().min(1),
89160
- configured: boolean(),
89161
- source: _enum(["native", "assigned"]),
89162
- state: _enum([
89163
- "active",
89164
- "stale",
89165
- "released",
89166
- "unavailable"
89167
- ]),
89168
- reason: string().nullable()
89169
- });
89170
89757
  var G711_SCALE_CORRECTION_DB = {
89171
89758
  PCMU: 20 * Math.log10(4),
89172
89759
  PCMA: 20 * Math.log10(8)
@@ -91550,6 +92137,88 @@ var googleBrokersEntryKind = defineEntryKind("brokers", {
91550
92137
  writeTrigger: "an operator adds or edits a Google OAuth broker"
91551
92138
  });
91552
92139
  //#endregion
92140
+ //#region src/google-instances.ts
92141
+ /**
92142
+ * The Google accounts as `integration-instances` (planning#408 P3): one instance per broker
92143
+ * record, over the SAME `brokers` records and ids the `broker` cap serves (`google_NNN`), so
92144
+ * nothing moves. The OAuth session lives beside the record (`oauthAccounts`), so an instance's
92145
+ * status is the registry's answer and a change re-seeds the registry.
92146
+ */
92147
+ function asTestResult(r) {
92148
+ return r.ok ? {
92149
+ success: true,
92150
+ status: "validated",
92151
+ ...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
92152
+ } : {
92153
+ success: false,
92154
+ status: "rejected",
92155
+ error: r.error ?? "Google rejected the check"
92156
+ };
92157
+ }
92158
+ function buildGoogleInstances(records, deps) {
92159
+ return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
92160
+ description: {
92161
+ role: "services",
92162
+ instanceMode: "multiple",
92163
+ kind: "account",
92164
+ canTest: true,
92165
+ kinds: [{
92166
+ id: GOOGLE_BROKER_KIND,
92167
+ label: GOOGLE_BROKER_KIND_LABEL,
92168
+ connectPath: deps.connectPath,
92169
+ disconnectPath: deps.disconnectPath
92170
+ }],
92171
+ allowRestoreId: true
92172
+ },
92173
+ formSchema: () => {
92174
+ const full = buildGoogleBrokerSettingsSchema(deps.publicOrigin());
92175
+ return {
92176
+ ...full,
92177
+ sections: full.sections.filter((s) => s.id !== "google-calendars-import")
92178
+ };
92179
+ },
92180
+ initialBody: (name) => ({
92181
+ name,
92182
+ clientId: "",
92183
+ clientSecret: "",
92184
+ calendarFilter: "",
92185
+ fromAddress: "",
92186
+ fromName: "",
92187
+ calendarIds: []
92188
+ }),
92189
+ mintId: deps.mintId,
92190
+ kindOf: () => GOOGLE_BROKER_KIND,
92191
+ health: (_body, id) => {
92192
+ const session = deps.session(id);
92193
+ if (session === "connected") return { status: {
92194
+ state: "ok",
92195
+ label: "Signed in"
92196
+ } };
92197
+ if (session === "reauth") return { status: {
92198
+ state: "error",
92199
+ label: "Sign in again",
92200
+ detail: "Google session expired"
92201
+ } };
92202
+ return { status: {
92203
+ state: "unknown",
92204
+ label: "Not signed in"
92205
+ } };
92206
+ },
92207
+ configured: (body) => body.clientId.length > 0 && (body.clientSecret ?? "").length > 0,
92208
+ onChanged: deps.onChanged,
92209
+ test: async ({ id, values }) => {
92210
+ const clientId = values["clientId"];
92211
+ const secret = values["clientSecret"];
92212
+ if (typeof clientId === "string" && clientId.length > 0 && typeof secret === "string" && secret.length > 0 && secret !== "__camstack_redacted__") return asTestResult(await deps.testClient(clientId, secret));
92213
+ if (id === null) return asTestResult({
92214
+ ok: false,
92215
+ error: "Enter the Client ID and Client secret first"
92216
+ });
92217
+ return asTestResult(await deps.testSaved(id));
92218
+ }
92219
+ });
92220
+ }
92221
+ //#endregion
91553
92222
  //#region src/migrations/calendar-ids-to-brokers.ts
91554
92223
  /**
91555
92224
  * The calendars a Google account imports are a fact of THAT account (D985): each broker record
@@ -91780,6 +92449,7 @@ var GoogleServicesAddon = class extends BaseAddon {
91780
92449
  oauthAccountsHandle = null;
91781
92450
  registry = new GoogleBrokerRegistry();
91782
92451
  brokerStore = null;
92452
+ instances = null;
91783
92453
  activeBrokers = [];
91784
92454
  /** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
91785
92455
  pendingStates = /* @__PURE__ */ new Map();
@@ -91827,6 +92497,7 @@ var GoogleServicesAddon = class extends BaseAddon {
91827
92497
  if (records === void 0) throw new Error("google-services: ctx.records is required (hub-only addon)");
91828
92498
  await records.declare({ brokers: googleBrokersEntryKind });
91829
92499
  this.brokerStore = GoogleBrokerStore.of(records);
92500
+ this.instances = this.buildInstances(records);
91830
92501
  this.activeBrokers = await this.loadPersistedBrokers();
91831
92502
  if ((await this.brokerStore.listResolved()).length === 0 && this.config.brokers.length > 0) {
91832
92503
  await this.brokerStore.replaceAll(this.config.brokers);
@@ -91889,6 +92560,10 @@ var GoogleServicesAddon = class extends BaseAddon {
91889
92560
  capability: brokerCapability,
91890
92561
  provider: this.buildBrokerProvider()
91891
92562
  },
92563
+ {
92564
+ capability: integrationInstancesCapability,
92565
+ provider: this.requireInstances().toProvider()
92566
+ },
91892
92567
  {
91893
92568
  capability: calendarSourceCapability,
91894
92569
  provider: calendarProvider
@@ -91908,6 +92583,35 @@ var GoogleServicesAddon = class extends BaseAddon {
91908
92583
  this.activeBrokers = await this.loadPersistedBrokers();
91909
92584
  this.registry.seedFrom(this.activeBrokers, accounts);
91910
92585
  }
92586
+ requireInstances() {
92587
+ if (this.instances === null) throw new Error("google-services: instances used before initialize");
92588
+ return this.instances;
92589
+ }
92590
+ /** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
92591
+ buildInstances(records) {
92592
+ const brokerProvider = this.buildBrokerProvider();
92593
+ return buildGoogleInstances(records, {
92594
+ connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
92595
+ disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`,
92596
+ publicOrigin: () => this.publicOrigin,
92597
+ mintId: () => this.registry.allocateId(),
92598
+ session: (id) => this.registry.isConnected(id) ? "connected" : this.registry.needsReauth(id) ? "reauth" : "none",
92599
+ onChanged: async () => {
92600
+ const accounts = this.registry.listOauthAccounts();
92601
+ this.activeBrokers = await this.loadPersistedBrokers();
92602
+ this.registry.seedFrom(this.activeBrokers, accounts);
92603
+ await this.persistOauthAccounts();
92604
+ },
92605
+ testSaved: async (id) => brokerProvider.testConnection({ id }),
92606
+ testClient: async (clientId, clientSecret) => brokerProvider.testSettings({
92607
+ kind: GOOGLE_BROKER_KIND,
92608
+ settings: {
92609
+ clientId,
92610
+ clientSecret
92611
+ }
92612
+ })
92613
+ });
92614
+ }
91911
92615
  buildBrokerProvider() {
91912
92616
  const ownerAddonId = this.ctx.id;
91913
92617
  const owns = (id) => this.registry.getEntry(id) !== null;