@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.
@@ -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-BaWhXZFm.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,11 +8810,17 @@ 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"
8763
8818
  * (`auth.accessToken`, `headers[].value`).
8764
8819
  *
8820
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8821
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8822
+ * key releases exactly that key's secret.
8823
+ *
8765
8824
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8766
8825
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8767
8826
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8774,11 +8833,13 @@ function parseSecretPath(path) {
8774
8833
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8775
8834
  return path.split(".").map((raw) => {
8776
8835
  const each = raw.endsWith("[]");
8777
- const key = each ? raw.slice(0, -2) : raw;
8836
+ const map = raw.endsWith("{}");
8837
+ const key = each || map ? raw.slice(0, -2) : raw;
8778
8838
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8779
8839
  return {
8780
8840
  key,
8781
- each
8841
+ each,
8842
+ map
8782
8843
  };
8783
8844
  });
8784
8845
  }
@@ -8793,6 +8854,128 @@ function secretArrayPrefixes(path) {
8793
8854
  }
8794
8855
  return prefixes;
8795
8856
  }
8857
+ function isRecord$4(value) {
8858
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8859
+ }
8860
+ function elementId(element, idKey, declared, prefix) {
8861
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8862
+ 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`);
8863
+ return id;
8864
+ }
8865
+ /**
8866
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8867
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8868
+ * result is a new body, `data` is untouched.
8869
+ */
8870
+ var REMOVE_LEAF = Symbol("remove-leaf");
8871
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8872
+ if (!isRecord$4(node)) return node;
8873
+ const segment = segments[index];
8874
+ if (segment === void 0) return node;
8875
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8876
+ const child = node[segment.key];
8877
+ const keys = [...walkedKeys, segment.key];
8878
+ const last = index === segments.length - 1;
8879
+ if (segment.each) {
8880
+ if (!Array.isArray(child)) return node;
8881
+ const prefix = keys.join(".");
8882
+ const idKey = arrayKeys[prefix];
8883
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8884
+ const mapped = child.map((element) => {
8885
+ const id = elementId(element, idKey, declared, prefix);
8886
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8887
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8888
+ });
8889
+ return {
8890
+ ...node,
8891
+ [segment.key]: mapped
8892
+ };
8893
+ }
8894
+ if (segment.map) {
8895
+ if (!isRecord$4(child)) return node;
8896
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8897
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8898
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8899
+ return [mapKey, visit({
8900
+ declared,
8901
+ concrete,
8902
+ value
8903
+ })];
8904
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8905
+ return {
8906
+ ...node,
8907
+ [segment.key]: Object.fromEntries(kept)
8908
+ };
8909
+ }
8910
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8911
+ if (last) {
8912
+ const replacement = visit({
8913
+ declared,
8914
+ concrete,
8915
+ value: child
8916
+ });
8917
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8918
+ return {
8919
+ ...node,
8920
+ [segment.key]: replacement
8921
+ };
8922
+ }
8923
+ return {
8924
+ ...node,
8925
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8926
+ };
8927
+ }
8928
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8929
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8930
+ let current = data;
8931
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8932
+ return current;
8933
+ }
8934
+ /**
8935
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8936
+ * record's refs never collide with the refs a legacy collection of the same name
8937
+ * already holds during an adoption.
8938
+ */
8939
+ function recordSecretCollection(kind) {
8940
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8941
+ }
8942
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8943
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8944
+ return buildSecretRef({
8945
+ owner,
8946
+ scope: {
8947
+ kind: "row",
8948
+ collection: recordSecretCollection(kind),
8949
+ rowId
8950
+ },
8951
+ fieldPath: concretePath
8952
+ });
8953
+ }
8954
+ /**
8955
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8956
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8957
+ * token. The form hands it to the secret field, which reveals the value by ref
8958
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8959
+ * stored secret (the records door reads a view as KEEP).
8960
+ *
8961
+ * ONE implementation for the entries service and for every addon that serves
8962
+ * its own instances (`RecordIntegrationInstances`).
8963
+ */
8964
+ function isRecord$3(value) {
8965
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8966
+ }
8967
+ function secretViewsOf(owner, kind, id, decl, data) {
8968
+ if (decl.secretPaths.length === 0) return data;
8969
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8970
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8971
+ return {
8972
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8973
+ v: 1,
8974
+ set: true
8975
+ };
8976
+ });
8977
+ return isRecord$3(shown) ? shown : data;
8978
+ }
8796
8979
  /**
8797
8980
  * The share-view scope kinds a share-link token can carry. The hub mints
8798
8981
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8865,7 +9048,8 @@ function isZodSecret(schema) {
8865
9048
  }
8866
9049
  /**
8867
9050
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8868
- * element of this array" (`connection.password`, `rows[].token`). A declared
9051
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9052
+ * map" (`applianceSecrets{}`). A declared
8869
9053
  * object or array is reported as one path and not descended into — everything
8870
9054
  * under it is secret.
8871
9055
  */
@@ -8876,6 +9060,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8876
9060
  const def = defOf(layer);
8877
9061
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8878
9062
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9063
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
8879
9064
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8880
9065
  else if (def.type === "intersection") {
8881
9066
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9015,7 +9200,7 @@ function defineRecordKinds(specs) {
9015
9200
  retention: spec.retention,
9016
9201
  deviceRemoval: spec.deviceRemoval,
9017
9202
  maxDataBytes: spec.maxDataBytes,
9018
- secretPaths: [...secretPaths],
9203
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9019
9204
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9020
9205
  transactFamily: spec.transactFamily,
9021
9206
  entry: spec.entry === void 0 ? void 0 : {
@@ -9060,6 +9245,7 @@ function defineEntryKind(name, spec) {
9060
9245
  ...spec.database === void 0 ? {} : { database: spec.database },
9061
9246
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9062
9247
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9248
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9063
9249
  entry: {
9064
9250
  title: spec.title,
9065
9251
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11735,7 +11921,7 @@ function userError(code, params, englishFallback) {
11735
11921
  return new CamstackError(code, params, englishFallback);
11736
11922
  }
11737
11923
  //#endregion
11738
- //#region ../types/dist/composition-BodkvOV0.mjs
11924
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11739
11925
  /**
11740
11926
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11741
11927
  *
@@ -12498,38 +12684,6 @@ function validateExpressionSource(src) {
12498
12684
  return null;
12499
12685
  }
12500
12686
  /**
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
12687
  * Statistics and integrals over a history read (D944).
12534
12688
  *
12535
12689
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12673,34 +12827,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12673
12827
  /** Actuator groups only. Readings leave this unset. */
12674
12828
  active: ActiveCriterionSchema.optional()
12675
12829
  }),
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
12830
  object({
12705
12831
  kind: literal("statistic"),
12706
12832
  stat: _enum(STATISTIC_STATS),
@@ -12924,7 +13050,7 @@ var CompositionSchema = object({
12924
13050
  kind: literal("existing"),
12925
13051
  device: CompositionSourceRefSchema
12926
13052
  })]),
12927
- features: array(CompositionFeatureSchema).min(1).max(16),
13053
+ features: array(CompositionFeatureSchema).min(0).max(16),
12928
13054
  /**
12929
13055
  * Present when the block is a helper template. Absent on a free
12930
13056
  * composition (the room example, device 4603). The editor reads this,
@@ -13259,6 +13385,205 @@ function buildCapturingReply(envelope) {
13259
13385
  };
13260
13386
  return wrapper;
13261
13387
  }
13388
+ /**
13389
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13390
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13391
+ *
13392
+ * Every instance is one record of the addon's own kind, keyed by the
13393
+ * `integrations` registry id. The records door does the secret work: a field the
13394
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13395
+ * deleting the row deletes the secret, and one row's value is never another's.
13396
+ *
13397
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13398
+ * token or the view the form was given KEEP the stored secret;
13399
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13400
+ */
13401
+ /** The most instances one addon lists (an integrations page, not a table). */
13402
+ var INSTANCES_LIST_MAX = 1e3;
13403
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13404
+ function topLevelSecretKeys(paths) {
13405
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13406
+ }
13407
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13408
+ function secretMapKeys(paths) {
13409
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13410
+ }
13411
+ /**
13412
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13413
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13414
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13415
+ */
13416
+ function secretMapPatch(value) {
13417
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13418
+ const out = {};
13419
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13420
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13421
+ return Object.keys(out).length > 0 ? out : null;
13422
+ }
13423
+ /** Does the kind's body schema carry an `enabled` field? */
13424
+ function declaresEnabled(kind) {
13425
+ const json = toJSONSchema(kind.schema, {
13426
+ unrepresentable: "any",
13427
+ io: "input"
13428
+ });
13429
+ const properties = Reflect.get(json, "properties");
13430
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13431
+ }
13432
+ var RecordIntegrationInstances = class {
13433
+ records;
13434
+ kind;
13435
+ hooks;
13436
+ handle;
13437
+ secretKeys;
13438
+ secretMaps;
13439
+ nameField;
13440
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13441
+ registryOwnsEnabled;
13442
+ constructor(records, kind, hooks) {
13443
+ this.records = records;
13444
+ this.kind = kind;
13445
+ this.hooks = hooks;
13446
+ this.handle = records.kind(kind);
13447
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13448
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13449
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13450
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13451
+ 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"`);
13452
+ }
13453
+ /**
13454
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13455
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13456
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13457
+ */
13458
+ async readResolved(id) {
13459
+ try {
13460
+ const record = await this.handle.getResolved(id);
13461
+ return record === null ? null : {
13462
+ id,
13463
+ body: record.data,
13464
+ enabled: record.data.enabled !== false,
13465
+ secrets: "resolved"
13466
+ };
13467
+ } catch (err) {
13468
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13469
+ const stored = await this.handle.get(id);
13470
+ if (stored === null) return null;
13471
+ return {
13472
+ id,
13473
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13474
+ enabled: stored.data.enabled !== false,
13475
+ secrets: "unavailable"
13476
+ };
13477
+ }
13478
+ }
13479
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13480
+ async getResolved(id) {
13481
+ return (await this.readResolved(id))?.body ?? null;
13482
+ }
13483
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13484
+ async listResolved() {
13485
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13486
+ const out = [];
13487
+ for (const record of page.records) {
13488
+ const read = await this.readResolved(record.id);
13489
+ if (read !== null) out.push(read);
13490
+ }
13491
+ return out;
13492
+ }
13493
+ viewOf(record) {
13494
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13495
+ const rawName = record.data[this.nameField];
13496
+ const kind = this.hooks.kindOf?.(record.data);
13497
+ const health = this.hooks.health?.(record.data, record.id);
13498
+ return {
13499
+ id: record.id,
13500
+ name: typeof rawName === "string" ? rawName : record.id,
13501
+ configured: this.hooks.configured(record.data),
13502
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13503
+ formValues: shown,
13504
+ ...kind !== void 0 ? { kind } : {},
13505
+ ...health?.status !== void 0 ? { status: health.status } : {},
13506
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13507
+ };
13508
+ }
13509
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13510
+ ...this.hooks.description,
13511
+ mintsId: true
13512
+ };
13513
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13514
+ listInstances = async () => {
13515
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13516
+ };
13517
+ getInstance = async ({ id }) => {
13518
+ const record = await this.handle.get(id);
13519
+ return record === null ? null : this.viewOf(record);
13520
+ };
13521
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13522
+ createInstance = async ({ id, name, kind }) => {
13523
+ const minted = id ?? this.hooks.mintId?.({
13524
+ name,
13525
+ ...kind !== void 0 ? { kind } : {}
13526
+ });
13527
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13528
+ await this.handle.put({
13529
+ id: minted,
13530
+ data: this.hooks.initialBody(name, kind)
13531
+ }, { ifRev: 0 });
13532
+ await this.hooks.onChanged?.(minted, "created");
13533
+ return { id: minted };
13534
+ };
13535
+ updateInstance = async ({ id, name, values }) => {
13536
+ const patch = {};
13537
+ for (const [key, value] of Object.entries(values)) {
13538
+ if (value === void 0) continue;
13539
+ if (this.secretMaps.has(key)) {
13540
+ const entries = secretMapPatch(value);
13541
+ if (entries !== null) patch[key] = entries;
13542
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13543
+ else if (isSecretClear(value)) patch[key] = null;
13544
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13545
+ }
13546
+ if (name !== void 0) patch[this.nameField] = name;
13547
+ await this.handle.patch(id, patch);
13548
+ await this.hooks.onChanged?.(id, "updated");
13549
+ return { ok: true };
13550
+ };
13551
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13552
+ setInstanceEnabled = async ({ id, enabled }) => {
13553
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13554
+ await this.handle.patch(id, { enabled });
13555
+ await this.hooks.onChanged?.(id, "enabled");
13556
+ return { ok: true };
13557
+ };
13558
+ /** The row and its keychain refs go together (the records door). */
13559
+ deleteInstance = async ({ id }) => {
13560
+ await this.handle.delete(id);
13561
+ await this.hooks.onChanged?.(id, "deleted");
13562
+ return { ok: true };
13563
+ };
13564
+ testInstance = async (input) => {
13565
+ if (this.hooks.test === void 0) return {
13566
+ success: false,
13567
+ status: "unsupported",
13568
+ error: "This integration declares no connection test"
13569
+ };
13570
+ return this.hooks.test(input);
13571
+ };
13572
+ /** The flat provider object to hand to `registerProvider`. */
13573
+ toProvider() {
13574
+ return {
13575
+ describe: this.describe,
13576
+ instanceFormSchema: this.instanceFormSchema,
13577
+ listInstances: this.listInstances,
13578
+ getInstance: this.getInstance,
13579
+ createInstance: this.createInstance,
13580
+ updateInstance: this.updateInstance,
13581
+ setInstanceEnabled: this.setInstanceEnabled,
13582
+ deleteInstance: this.deleteInstance,
13583
+ testInstance: this.testInstance
13584
+ };
13585
+ }
13586
+ };
13262
13587
  var EncodeProfileSchema = object({
13263
13588
  video: object({
13264
13589
  codec: _enum([
@@ -48712,49 +49037,6 @@ function actuator(input) {
48712
49037
  };
48713
49038
  }
48714
49039
  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
49040
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
48759
49041
  DeviceType.Camera;
48760
49042
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -48782,6 +49064,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
48782
49064
  }).optional()
48783
49065
  })) });
48784
49066
  /**
49067
+ * A saved Customize assignment (D936). The schema is the type: callers use
49068
+ * `z.infer`, they do not restate the shape.
49069
+ */
49070
+ var OverrideSourceSchema = object({
49071
+ addonId: string().min(1),
49072
+ stableId: string().min(1),
49073
+ cap: string().min(1),
49074
+ fieldPath: string().min(1)
49075
+ });
49076
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
49077
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
49078
+ var ThresholdTransformDraftSchema = object({
49079
+ kind: literal("threshold"),
49080
+ comparison: _enum(["gte", "lte"]),
49081
+ threshold: number(),
49082
+ startDwellMs: number().nonnegative(),
49083
+ stopDwellMs: number().nonnegative()
49084
+ });
49085
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
49086
+ IdentityTransformDraftSchema,
49087
+ InvertTransformDraftSchema,
49088
+ ThresholdTransformDraftSchema,
49089
+ object({
49090
+ kind: literal("linear"),
49091
+ scale: number(),
49092
+ offset: number(),
49093
+ clampMin: number().optional(),
49094
+ clampMax: number().optional()
49095
+ }),
49096
+ object({
49097
+ kind: literal("enum-map"),
49098
+ mapping: record(string(), string().nullable())
49099
+ }),
49100
+ object({ kind: literal("number") })
49101
+ ]);
49102
+ var ScalarOverrideDraftSchema = object({
49103
+ kind: literal("scalar"),
49104
+ cap: string().min(1),
49105
+ targetId: string().min(1),
49106
+ source: OverrideSourceSchema,
49107
+ transform: CustomizeTransformDraftSchema
49108
+ });
49109
+ var KeyedArrayOverrideDraftSchema = object({
49110
+ kind: literal("keyedArray"),
49111
+ cap: string().min(1),
49112
+ targetId: string().min(1),
49113
+ key: string().min(1),
49114
+ label: string().min(1),
49115
+ fieldPath: string().min(1),
49116
+ source: OverrideSourceSchema,
49117
+ transform: CustomizeTransformDraftSchema
49118
+ });
49119
+ var PulseOverrideDraftSchema = object({
49120
+ kind: literal("pulse"),
49121
+ cap: string().min(1),
49122
+ targetId: string().min(1),
49123
+ enabled: boolean(),
49124
+ debounceMs: number().nonnegative(),
49125
+ edges: array(_enum([
49126
+ "rising",
49127
+ "button",
49128
+ "native-event"
49129
+ ])).min(1).optional(),
49130
+ sources: array(OverrideSourceSchema).min(1)
49131
+ });
49132
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
49133
+ "in-addition",
49134
+ "confirm",
49135
+ "replace"
49136
+ ]).optional() });
49137
+ discriminatedUnion("kind", [
49138
+ ScalarOverrideDraftSchema,
49139
+ KeyedArrayOverrideDraftSchema,
49140
+ PulseOverrideDraftSchema,
49141
+ object({
49142
+ kind: literal("span"),
49143
+ cap: string().min(1),
49144
+ targetId: string().min(1),
49145
+ timeoutMs: number().nonnegative(),
49146
+ cooldownMs: number().nonnegative().optional(),
49147
+ sources: array(SpanSourceDraftSchema)
49148
+ })
49149
+ ]);
49150
+ object({
49151
+ targetId: string().min(1),
49152
+ configured: boolean(),
49153
+ source: _enum(["native", "assigned"]),
49154
+ state: _enum([
49155
+ "active",
49156
+ "stale",
49157
+ "released",
49158
+ "unavailable"
49159
+ ]),
49160
+ reason: string().nullable()
49161
+ });
49162
+ /**
49163
+ * What a running-threshold proposal says (planning#405). The schema is the
49164
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
49165
+ *
49166
+ * A proposal is read-only advice: it carries the numbers it was derived from
49167
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
49168
+ * WHY with a typed reason and the catalog defaults, never an invented number.
49169
+ */
49170
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
49171
+ var RunningProposalConfidenceSchema = _enum([
49172
+ "high",
49173
+ "medium",
49174
+ "low"
49175
+ ]);
49176
+ var RunningProposalInsufficientReasonSchema = _enum([
49177
+ "no-history",
49178
+ "span-too-short",
49179
+ "truncated",
49180
+ "no-idle-floor",
49181
+ "no-runs",
49182
+ "no-separation"
49183
+ ]);
49184
+ var RunningProposalReplaySchema = object({
49185
+ /** Intervals the real dwell expression held true over the window. */
49186
+ intervals: number().int().nonnegative(),
49187
+ /** Intervals outside every run, or a second interval inside one. */
49188
+ extra: number().int().nonnegative(),
49189
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
49190
+ missed: number().int().nonnegative()
49191
+ });
49192
+ var RunningProposalEvidenceSchema = object({
49193
+ tier: RunningProposalTierSchema,
49194
+ spanMs: number().nonnegative(),
49195
+ from: number(),
49196
+ to: number(),
49197
+ points: number().int().nonnegative(),
49198
+ runsFound: number().int().nonnegative(),
49199
+ pausesFound: number().int().nonnegative(),
49200
+ longestPauseMs: number().nonnegative(),
49201
+ idleFloorW: number(),
49202
+ noiseCeilingW: number(),
49203
+ plateauW: number(),
49204
+ replay: RunningProposalReplaySchema
49205
+ });
49206
+ var RunningProposalInsufficientEvidenceSchema = object({
49207
+ spanMs: number().nonnegative(),
49208
+ points: number().int().nonnegative(),
49209
+ runsFound: number().int().nonnegative()
49210
+ });
49211
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
49212
+ state: literal("proposal"),
49213
+ transform: ThresholdTransformDraftSchema,
49214
+ evidence: RunningProposalEvidenceSchema,
49215
+ confidence: RunningProposalConfidenceSchema,
49216
+ /** Why the confidence is not higher; empty when nothing held it back. */
49217
+ notes: array(string())
49218
+ }), object({
49219
+ state: literal("insufficient"),
49220
+ reason: RunningProposalInsufficientReasonSchema,
49221
+ evidence: RunningProposalInsufficientEvidenceSchema,
49222
+ /** The catalog defaults, offered as what stands when nothing better is known. */
49223
+ fallback: ThresholdTransformDraftSchema
49224
+ })]);
49225
+ /**
48785
49226
  * The report of a one-shot "move this owner's plaintext secrets into the
48786
49227
  * keychain" run (D878). COUNTS only - never a value, never a ref.
48787
49228
  *
@@ -49071,6 +49512,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49071
49512
  kind: "mutation",
49072
49513
  auth: "admin",
49073
49514
  access: "view"
49515
+ }), method(object({
49516
+ deviceId: number().int(),
49517
+ cap: string().min(1),
49518
+ fieldPath: string().min(1),
49519
+ deviceType: _enum(DeviceType).optional()
49520
+ }), RunningThresholdProposalSchema, {
49521
+ auth: "admin",
49522
+ access: "view"
49074
49523
  }), method(object({
49075
49524
  blockId: string().min(1),
49076
49525
  trace: AutomationRunTraceSchema.optional(),
@@ -49879,7 +50328,27 @@ method(object({
49879
50328
  * exposes only the COMMON surface: link state, supported device
49880
50329
  * kinds (so the UI can filter the device picker), and per-device
49881
50330
  * expose/unexpose.
49882
- */
50331
+ *
50332
+ * INSTANCES. One export addon can serve several independent targets (a
50333
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
50334
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
50335
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
50336
+ * default instance's); present = that instance; an id the provider does not
50337
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
50338
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
50339
+ * change always names the target it changes. A single-instance provider
50340
+ * accepts an absent id on a read or its own id, and refuses any other.
50341
+ */
50342
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
50343
+ var InstanceIdSchema$1 = string().min(1);
50344
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
50345
+ /** One independent export target of an addon. */
50346
+ var ExportInstanceSchema = object({
50347
+ instanceId: InstanceIdSchema$1,
50348
+ name: string(),
50349
+ enabled: boolean(),
50350
+ kind: string().optional()
50351
+ });
49883
50352
  var LinkStateSchema = _enum([
49884
50353
  "unlinked",
49885
50354
  "linked",
@@ -49930,10 +50399,14 @@ var ExposedDeviceSchema = object({
49930
50399
  capabilities: array(string()).optional()
49931
50400
  });
49932
50401
  var ExposeInputSchema = object({
50402
+ instanceId: InstanceIdSchema$1,
49933
50403
  deviceId: string(),
49934
50404
  capabilities: array(string()).optional()
49935
50405
  });
49936
- var UnexposeInputSchema = object({ deviceId: string() });
50406
+ var UnexposeInputSchema = object({
50407
+ instanceId: InstanceIdSchema$1,
50408
+ deviceId: string()
50409
+ });
49937
50410
  /**
49938
50411
  * What an exporter may expose besides devices (D968). An action and a
49939
50412
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -49962,15 +50435,21 @@ var ExposedEntitySchema = object({
49962
50435
  entity: ExportEntityRefSchema,
49963
50436
  exposedAs: string().optional()
49964
50437
  });
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(), {
50438
+ var ExposeEntityInputSchema = object({
50439
+ instanceId: InstanceIdSchema$1,
50440
+ entity: ExportEntityRefSchema
50441
+ });
50442
+ var UnexposeEntityInputSchema = object({
50443
+ instanceId: InstanceIdSchema$1,
50444
+ entity: ExportEntityRefSchema
50445
+ });
50446
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
49968
50447
  kind: "mutation",
49969
50448
  auth: "admin"
49970
50449
  }), method(UnexposeInputSchema, _void(), {
49971
50450
  kind: "mutation",
49972
50451
  auth: "admin"
49973
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50452
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
49974
50453
  kind: "mutation",
49975
50454
  auth: "admin"
49976
50455
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -53171,15 +53650,15 @@ var FanPresetCaptureResultSchema = object({
53171
53650
  items: array(FanCaptureItemSchema),
53172
53651
  omitted: array(FanCaptureOmissionSchema)
53173
53652
  });
53174
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
53653
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
53175
53654
  DeviceType.Fan;
53176
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53655
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53177
53656
  kind: "mutation",
53178
53657
  auth: "admin"
53179
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53658
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53180
53659
  kind: "mutation",
53181
53660
  auth: "protected"
53182
- }), method(deviceInput$1.extend({
53661
+ }), method(deviceInput$2.extend({
53183
53662
  fields: array(FanCaptureFieldSchema).optional(),
53184
53663
  steps: array(ActionStepSchema).max(20).default([])
53185
53664
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -53703,13 +54182,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53703
54182
  */
53704
54183
  var ConfigSchemaPassthrough$3 = custom();
53705
54184
  var IntegrationInstanceIdSchema = string().min(1);
54185
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54186
+ /**
54187
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54188
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54189
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
54190
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
54191
+ * instance id.
54192
+ */
54193
+ var IntegrationInstanceKindSchema = object({
54194
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
54195
+ id: string().min(1),
54196
+ label: string().min(1),
54197
+ /** The kind signs in through a server-held interactive session. */
54198
+ signIn: object({
54199
+ basePath: string().min(1),
54200
+ methods: array(_enum(["device-code", "proxy"])).optional()
54201
+ }).optional(),
54202
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
54203
+ connectPath: string().min(1).optional(),
54204
+ /** Relative hub path to disconnect that account. */
54205
+ disconnectPath: string().min(1).optional()
54206
+ });
53706
54207
  var IntegrationInstanceDescriptionSchema = object({
54208
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54209
+ role: _enum([
54210
+ "devices",
54211
+ "services",
54212
+ "both"
54213
+ ]),
54214
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54215
+ discovery: object({
54216
+ mode: _enum([
54217
+ "none",
54218
+ "lan",
54219
+ "account"
54220
+ ]),
54221
+ scopedToInstance: boolean()
54222
+ }).optional(),
53707
54223
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
53708
- instanceMode: _enum(["single", "multiple"]),
54224
+ instanceMode: IntegrationInstanceModeSchema,
53709
54225
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
53710
54226
  kind: string(),
53711
54227
  /** True when `testInstance` really probes the remote. */
53712
- canTest: boolean()
54228
+ canTest: boolean(),
54229
+ /**
54230
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
54231
+ * Add flow asks which first and sends it as `createInstance.kind`.
54232
+ */
54233
+ kinds: array(IntegrationInstanceKindSchema).optional(),
54234
+ /**
54235
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
54236
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
54237
+ */
54238
+ allowRestoreId: boolean().optional(),
54239
+ /**
54240
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
54241
+ * send an id (the UI then mints an opaque one).
54242
+ */
54243
+ mintsId: boolean().optional()
53713
54244
  });
53714
54245
  var IntegrationInstanceStatusSchema = object({
53715
54246
  state: _enum([
@@ -53718,7 +54249,9 @@ var IntegrationInstanceStatusSchema = object({
53718
54249
  "error",
53719
54250
  "unknown"
53720
54251
  ]),
53721
- detail: string().optional()
54252
+ detail: string().optional(),
54253
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
54254
+ label: string().optional()
53722
54255
  });
53723
54256
  var IntegrationInstanceSchema = object({
53724
54257
  id: IntegrationInstanceIdSchema,
@@ -53726,13 +54259,18 @@ var IntegrationInstanceSchema = object({
53726
54259
  /** The addon's own rule (an URL is present, a client id is present...). */
53727
54260
  configured: boolean(),
53728
54261
  /**
53729
- * The instance's own on/off switch (D996): always present, derived from the record body
53730
- * (`enabled !== false`, so a row written before the field existed is on).
54262
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
54263
+ * so a row written before the field existed is on). Absent when the `integrations` registry
54264
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
53731
54265
  */
53732
- enabled: boolean(),
54266
+ enabled: boolean().optional(),
54267
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
54268
+ kind: string().optional(),
53733
54269
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
53734
54270
  formValues: record(string(), unknown()),
53735
- status: IntegrationInstanceStatusSchema.optional()
54271
+ status: IntegrationInstanceStatusSchema.optional(),
54272
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
54273
+ lastCheckedAt: number().optional()
53736
54274
  });
53737
54275
  var IntegrationInstancePatchSchema = object({
53738
54276
  id: IntegrationInstanceIdSchema,
@@ -53740,31 +54278,57 @@ var IntegrationInstancePatchSchema = object({
53740
54278
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
53741
54279
  values: record(string(), unknown())
53742
54280
  });
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
- });
54281
+ var integrationInstancesCapability = {
54282
+ name: "integration-instances",
54283
+ scope: "system",
54284
+ mode: "collection",
54285
+ methods: {
54286
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
54287
+ /** The form; `id: null` is the create form. */
54288
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54289
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54290
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54291
+ /**
54292
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
54293
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
54294
+ */
54295
+ createInstance: method(object({
54296
+ id: IntegrationInstanceIdSchema.optional(),
54297
+ name: string().min(1),
54298
+ kind: string().min(1).optional()
54299
+ }), object({ id: string() }), {
54300
+ kind: "mutation",
54301
+ auth: "admin"
54302
+ }),
54303
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
54304
+ kind: "mutation",
54305
+ auth: "admin"
54306
+ }),
54307
+ /**
54308
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
54309
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
54310
+ */
54311
+ setInstanceEnabled: method(object({
54312
+ id: IntegrationInstanceIdSchema,
54313
+ enabled: boolean()
54314
+ }), object({ ok: literal(true) }), {
54315
+ kind: "mutation",
54316
+ auth: "admin"
54317
+ }),
54318
+ /** Delete the record and its keychain refs together. */
54319
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
54320
+ kind: "mutation",
54321
+ auth: "admin"
54322
+ }),
54323
+ testInstance: optionalMethod(object({
54324
+ id: IntegrationInstanceIdSchema.nullable(),
54325
+ values: record(string(), unknown())
54326
+ }), TestConnectionResultSchema$1, {
54327
+ kind: "mutation",
54328
+ auth: "admin"
54329
+ })
54330
+ }
54331
+ };
53768
54332
  /**
53769
54333
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
53770
54334
  * session (server-initiated offer pattern, identical to the main
@@ -55563,6 +56127,11 @@ var LogEntrySchema = object({
55563
56127
  * flight, and not asking for the next slice ends the scan.
55564
56128
  */
55565
56129
  var LogReadQuerySchema = object({
56130
+ /**
56131
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
56132
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
56133
+ */
56134
+ instanceId: string().min(1).max(200).optional(),
55566
56135
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
55567
56136
  requestId: string().min(1).max(64).optional(),
55568
56137
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -55625,6 +56194,8 @@ var LogReadResultSchema = object({
55625
56194
  /** True when `cancelRead` ended this slice early. */
55626
56195
  cancelled: boolean().optional()
55627
56196
  });
56197
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
56198
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
55628
56199
  var LogReadCapabilitiesSchema = object({
55629
56200
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
55630
56201
  readable: boolean(),
@@ -55643,7 +56214,7 @@ var LogReadCapabilitiesSchema = object({
55643
56214
  /** How far back the store reaches, when the provider knows (days). */
55644
56215
  retentionDays: number().nullable()
55645
56216
  });
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() }), {
56217
+ 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
56218
  kind: "mutation",
55648
56219
  auth: "admin"
55649
56220
  });
@@ -67215,6 +67786,49 @@ object({
67215
67786
  DeviceType.PetFeeder,
67216
67787
  DeviceType.Heater
67217
67788
  ].filter((type) => type !== DeviceType.Sensor);
67789
+ var PowerSourceRefusalReasonSchema = _enum([
67790
+ "no-power-source",
67791
+ "switch-missing",
67792
+ "switch-offline",
67793
+ "switch-unknown",
67794
+ "restart-in-progress",
67795
+ "switch-call-failed"
67796
+ ]);
67797
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
67798
+ accepted: literal(false),
67799
+ reason: PowerSourceRefusalReasonSchema,
67800
+ /** One line for the operator, naming the switch. */
67801
+ message: string()
67802
+ })]);
67803
+ var PowerSourceInfoSchema = object({
67804
+ /** False when the device has no power source linked; the rest is then empty. */
67805
+ linked: boolean(),
67806
+ switchDeviceId: number().nullable(),
67807
+ switchName: string().nullable(),
67808
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
67809
+ switchOn: boolean().nullable(),
67810
+ switchOnline: boolean().nullable(),
67811
+ restartInProgress: boolean(),
67812
+ offSeconds: number().int()
67813
+ });
67814
+ object({
67815
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
67816
+ switchOn: boolean().nullable(),
67817
+ offSeconds: number().int(),
67818
+ lastFetchedAt: number()
67819
+ });
67820
+ 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;
67821
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
67822
+ systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67823
+ kind: "mutation",
67824
+ auth: "admin"
67825
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67826
+ kind: "mutation",
67827
+ auth: "admin"
67828
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67829
+ kind: "mutation",
67830
+ auth: "admin"
67831
+ });
67218
67832
  /**
67219
67833
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
67220
67834
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -75281,6 +75895,25 @@ function buildDescriptor(capName, kind) {
75281
75895
  * contributes one entry per bound cap present in this map.
75282
75896
  */
75283
75897
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
75898
+ /**
75899
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
75900
+ * shared by the admin UI and the viewer. A role is data on the wire; each
75901
+ * client resolves its name from `label` (English and Italian side by side) and
75902
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
75903
+ * server sends an id, a client maps it to its own icon set.
75904
+ *
75905
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
75906
+ * pictogram): a client that can draw text shows it, one that cannot falls back
75907
+ * to `icon`.
75908
+ */
75909
+ function digit(n) {
75910
+ return {
75911
+ label: t(n, n),
75912
+ icon: "hash",
75913
+ legend: n
75914
+ };
75915
+ }
75916
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
75284
75917
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
75285
75918
  var btn = (idSuffix, row, col, init) => ({
75286
75919
  idSuffix,
@@ -75408,6 +76041,34 @@ discriminatedUnion("kind", [object({
75408
76041
  kind: literal("pronto"),
75409
76042
  code: string()
75410
76043
  })]);
76044
+ /**
76045
+ * Propose a running threshold from a device's own power history (planning#405).
76046
+ *
76047
+ * Pure and deterministic: the same series gives the same answer. The series is
76048
+ * a HELD-VALUE step series — a point's value stands until the next point — which
76049
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
76050
+ * count of samples.
76051
+ *
76052
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
76053
+ * near F. A device that is not near one level most of the time has no floor.
76054
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
76055
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
76056
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
76057
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
76058
+ * grid a person would type; stop dwell covers the longest pause; start dwell
76059
+ * covers the longest standby blip above T.
76060
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
76061
+ * run must hold exactly one true interval and nothing may flap outside.
76062
+ */
76063
+ var SECOND_MS = 1e3;
76064
+ var MINUTE_MS$1 = 60 * SECOND_MS;
76065
+ 24 * (60 * MINUTE_MS$1);
76066
+ 10 * MINUTE_MS$1;
76067
+ 30 * SECOND_MS;
76068
+ 60 * SECOND_MS;
76069
+ 20 * MINUTE_MS$1;
76070
+ 10 * SECOND_MS;
76071
+ 10 * SECOND_MS;
75411
76072
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75412
76073
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
75413
76074
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -76949,6 +77610,13 @@ Object.freeze({
76949
77610
  access: "create",
76950
77611
  role: "admin"
76951
77612
  },
77613
+ "coreBlocks.proposeRunningThreshold": {
77614
+ capName: "core-blocks",
77615
+ capScope: "system",
77616
+ addonId: null,
77617
+ access: "view",
77618
+ role: "admin"
77619
+ },
76952
77620
  "coreBlocks.reportAutomationRun": {
76953
77621
  capName: "core-blocks",
76954
77622
  capScope: "system",
@@ -77460,6 +78128,13 @@ Object.freeze({
77460
78128
  access: "view",
77461
78129
  role: "admin"
77462
78130
  },
78131
+ "deviceExport.listInstances": {
78132
+ capName: "device-export",
78133
+ capScope: "system",
78134
+ addonId: null,
78135
+ access: "view",
78136
+ role: "admin"
78137
+ },
77463
78138
  "deviceExport.listSupportedDeviceKinds": {
77464
78139
  capName: "device-export",
77465
78140
  capScope: "system",
@@ -86407,6 +87082,11 @@ Object.freeze({
86407
87082
  form: "single",
86408
87083
  optional: false
86409
87084
  }],
87085
+ "coreBlocks.proposeRunningThreshold": [{
87086
+ name: "deviceId",
87087
+ form: "single",
87088
+ optional: false
87089
+ }],
86410
87090
  "cover.close": [{
86411
87091
  name: "deviceId",
86412
87092
  form: "single",
@@ -89070,99 +89750,6 @@ Object.freeze({
89070
89750
  "network-access": "ingress",
89071
89751
  "smtp-provider": "email"
89072
89752
  });
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
89753
  var G711_SCALE_CORRECTION_DB = {
89167
89754
  PCMU: 20 * Math.log10(4),
89168
89755
  PCMA: 20 * Math.log10(8)
@@ -91546,6 +92133,88 @@ var googleBrokersEntryKind = defineEntryKind("brokers", {
91546
92133
  writeTrigger: "an operator adds or edits a Google OAuth broker"
91547
92134
  });
91548
92135
  //#endregion
92136
+ //#region src/google-instances.ts
92137
+ /**
92138
+ * The Google accounts as `integration-instances` (planning#408 P3): one instance per broker
92139
+ * record, over the SAME `brokers` records and ids the `broker` cap serves (`google_NNN`), so
92140
+ * nothing moves. The OAuth session lives beside the record (`oauthAccounts`), so an instance's
92141
+ * status is the registry's answer and a change re-seeds the registry.
92142
+ */
92143
+ function asTestResult(r) {
92144
+ return r.ok ? {
92145
+ success: true,
92146
+ status: "validated",
92147
+ ...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
92148
+ } : {
92149
+ success: false,
92150
+ status: "rejected",
92151
+ error: r.error ?? "Google rejected the check"
92152
+ };
92153
+ }
92154
+ function buildGoogleInstances(records, deps) {
92155
+ return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
92156
+ description: {
92157
+ role: "services",
92158
+ instanceMode: "multiple",
92159
+ kind: "account",
92160
+ canTest: true,
92161
+ kinds: [{
92162
+ id: GOOGLE_BROKER_KIND,
92163
+ label: GOOGLE_BROKER_KIND_LABEL,
92164
+ connectPath: deps.connectPath,
92165
+ disconnectPath: deps.disconnectPath
92166
+ }],
92167
+ allowRestoreId: true
92168
+ },
92169
+ formSchema: () => {
92170
+ const full = buildGoogleBrokerSettingsSchema(deps.publicOrigin());
92171
+ return {
92172
+ ...full,
92173
+ sections: full.sections.filter((s) => s.id !== "google-calendars-import")
92174
+ };
92175
+ },
92176
+ initialBody: (name) => ({
92177
+ name,
92178
+ clientId: "",
92179
+ clientSecret: "",
92180
+ calendarFilter: "",
92181
+ fromAddress: "",
92182
+ fromName: "",
92183
+ calendarIds: []
92184
+ }),
92185
+ mintId: deps.mintId,
92186
+ kindOf: () => GOOGLE_BROKER_KIND,
92187
+ health: (_body, id) => {
92188
+ const session = deps.session(id);
92189
+ if (session === "connected") return { status: {
92190
+ state: "ok",
92191
+ label: "Signed in"
92192
+ } };
92193
+ if (session === "reauth") return { status: {
92194
+ state: "error",
92195
+ label: "Sign in again",
92196
+ detail: "Google session expired"
92197
+ } };
92198
+ return { status: {
92199
+ state: "unknown",
92200
+ label: "Not signed in"
92201
+ } };
92202
+ },
92203
+ configured: (body) => body.clientId.length > 0 && (body.clientSecret ?? "").length > 0,
92204
+ onChanged: deps.onChanged,
92205
+ test: async ({ id, values }) => {
92206
+ const clientId = values["clientId"];
92207
+ const secret = values["clientSecret"];
92208
+ if (typeof clientId === "string" && clientId.length > 0 && typeof secret === "string" && secret.length > 0 && secret !== "__camstack_redacted__") return asTestResult(await deps.testClient(clientId, secret));
92209
+ if (id === null) return asTestResult({
92210
+ ok: false,
92211
+ error: "Enter the Client ID and Client secret first"
92212
+ });
92213
+ return asTestResult(await deps.testSaved(id));
92214
+ }
92215
+ });
92216
+ }
92217
+ //#endregion
91549
92218
  //#region src/migrations/calendar-ids-to-brokers.ts
91550
92219
  /**
91551
92220
  * The calendars a Google account imports are a fact of THAT account (D985): each broker record
@@ -91776,6 +92445,7 @@ var GoogleServicesAddon = class extends BaseAddon {
91776
92445
  oauthAccountsHandle = null;
91777
92446
  registry = new GoogleBrokerRegistry();
91778
92447
  brokerStore = null;
92448
+ instances = null;
91779
92449
  activeBrokers = [];
91780
92450
  /** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
91781
92451
  pendingStates = /* @__PURE__ */ new Map();
@@ -91823,6 +92493,7 @@ var GoogleServicesAddon = class extends BaseAddon {
91823
92493
  if (records === void 0) throw new Error("google-services: ctx.records is required (hub-only addon)");
91824
92494
  await records.declare({ brokers: googleBrokersEntryKind });
91825
92495
  this.brokerStore = GoogleBrokerStore.of(records);
92496
+ this.instances = this.buildInstances(records);
91826
92497
  this.activeBrokers = await this.loadPersistedBrokers();
91827
92498
  if ((await this.brokerStore.listResolved()).length === 0 && this.config.brokers.length > 0) {
91828
92499
  await this.brokerStore.replaceAll(this.config.brokers);
@@ -91885,6 +92556,10 @@ var GoogleServicesAddon = class extends BaseAddon {
91885
92556
  capability: brokerCapability,
91886
92557
  provider: this.buildBrokerProvider()
91887
92558
  },
92559
+ {
92560
+ capability: integrationInstancesCapability,
92561
+ provider: this.requireInstances().toProvider()
92562
+ },
91888
92563
  {
91889
92564
  capability: calendarSourceCapability,
91890
92565
  provider: calendarProvider
@@ -91904,6 +92579,35 @@ var GoogleServicesAddon = class extends BaseAddon {
91904
92579
  this.activeBrokers = await this.loadPersistedBrokers();
91905
92580
  this.registry.seedFrom(this.activeBrokers, accounts);
91906
92581
  }
92582
+ requireInstances() {
92583
+ if (this.instances === null) throw new Error("google-services: instances used before initialize");
92584
+ return this.instances;
92585
+ }
92586
+ /** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
92587
+ buildInstances(records) {
92588
+ const brokerProvider = this.buildBrokerProvider();
92589
+ return buildGoogleInstances(records, {
92590
+ connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
92591
+ disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`,
92592
+ publicOrigin: () => this.publicOrigin,
92593
+ mintId: () => this.registry.allocateId(),
92594
+ session: (id) => this.registry.isConnected(id) ? "connected" : this.registry.needsReauth(id) ? "reauth" : "none",
92595
+ onChanged: async () => {
92596
+ const accounts = this.registry.listOauthAccounts();
92597
+ this.activeBrokers = await this.loadPersistedBrokers();
92598
+ this.registry.seedFrom(this.activeBrokers, accounts);
92599
+ await this.persistOauthAccounts();
92600
+ },
92601
+ testSaved: async (id) => brokerProvider.testConnection({ id }),
92602
+ testClient: async (clientId, clientSecret) => brokerProvider.testSettings({
92603
+ kind: GOOGLE_BROKER_KIND,
92604
+ settings: {
92605
+ clientId,
92606
+ clientSecret
92607
+ }
92608
+ })
92609
+ });
92610
+ }
91907
92611
  buildBrokerProvider() {
91908
92612
  const ownerAddonId = this.ctx.id;
91909
92613
  const owns = (id) => this.registry.getEntry(id) !== null;