@camstack/addon-provider-enpal 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.10] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - Internal: the connection form comment names the instance form; no behaviour change.
15
+
16
+ ## [0.1.9] - 2026-11-05
17
+
18
+ ### Changed
19
+
20
+ - Copying an integration into its record reads the leftover rows through the migration-only door and resolves their secrets in this addon
21
+
10
22
  ## [0.1.8] - 2026-11-04
11
23
 
12
24
  ### Changed
package/dist/addon.js CHANGED
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
5981
5981
  var ZodFirstPartyTypeKind;
5982
5982
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5983
5983
  //#endregion
5984
- //#region ../types/dist/sleep-D1jjWAr8.mjs
5984
+ //#region ../types/dist/sleep-DEZaLbiu.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).
@@ -6790,6 +6790,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6790
6790
  }
6791
6791
  return out;
6792
6792
  }
6793
+ /** Every scope, in the order the guard and the docs list them. */
6794
+ var SCOPED_SETTINGS_SCOPES = [
6795
+ "cluster",
6796
+ "device-defaults",
6797
+ "node"
6798
+ ];
6793
6799
  /**
6794
6800
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6795
6801
  *
@@ -7681,6 +7687,13 @@ var BaseAddon = class {
7681
7687
  */
7682
7688
  secretStatePaths = [];
7683
7689
  _addonSecretTree = null;
7690
+ /**
7691
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7692
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7693
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7694
+ */
7695
+ _scopedByCap = /* @__PURE__ */ new Map();
7696
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7684
7697
  _deviceSecretTree = null;
7685
7698
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7686
7699
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7750,6 +7763,7 @@ var BaseAddon = class {
7750
7763
  this._ctx = context;
7751
7764
  this._secretSettings = null;
7752
7765
  }
7766
+ await this.loadScopedSchemas();
7753
7767
  await this.resolveConfig();
7754
7768
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7755
7769
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7759,6 +7773,7 @@ var BaseAddon = class {
7759
7773
  const normalized = normalizeAddonInitResult(result);
7760
7774
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7761
7775
  this._registeredCapNames = providers.map((p) => p.capability.name);
7776
+ await this.loadScopedSchemas();
7762
7777
  return normalized;
7763
7778
  }
7764
7779
  /**
@@ -7847,6 +7862,16 @@ var BaseAddon = class {
7847
7862
  schema(schema) {
7848
7863
  return schema;
7849
7864
  }
7865
+ /**
7866
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7867
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7868
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7869
+ * must already know which keys are per node. Names the providers register at init are added
7870
+ * once known.
7871
+ */
7872
+ scopedSettingsCapabilities() {
7873
+ return [];
7874
+ }
7850
7875
  /** Override to provide global-level settings UI schema. */
7851
7876
  globalSettingsSchema(_cap) {
7852
7877
  return null;
@@ -7890,7 +7915,8 @@ var BaseAddon = class {
7890
7915
  return [];
7891
7916
  }
7892
7917
  async getGlobalSettings(overlay, cap, nodeId) {
7893
- const schema = this.globalSettingsSchema(cap);
7918
+ await this.loadScopedSchemas(cap);
7919
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7894
7920
  if (!schema) return { sections: [] };
7895
7921
  const projected = await this.readRawProjectedStore(nodeId, cap);
7896
7922
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7925,10 +7951,9 @@ var BaseAddon = class {
7925
7951
  *
7926
7952
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7927
7953
  * needs its connection fields, the default integration the addon's own sections. It is the
7928
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7929
- * unchanged (`integrations.setSettings` for an account integration).
7954
+ * SHAPE of the form only — the values live on the instance record.
7930
7955
  */
7931
- async integrationSettingsSchema(_instance) {
7956
+ async integrationFormSchema(_instance) {
7932
7957
  return null;
7933
7958
  }
7934
7959
  /**
@@ -7974,7 +7999,7 @@ var BaseAddon = class {
7974
7999
  * per-instance schema when it declares one, else its global schema (what they always showed).
7975
8000
  */
7976
8001
  async getIntegrationFormSchema(instance, nodeId) {
7977
- const scoped = await this.integrationSettingsSchema(instance);
8002
+ const scoped = await this.integrationFormSchema(instance);
7978
8003
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7979
8004
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7980
8005
  }
@@ -8022,6 +8047,7 @@ var BaseAddon = class {
8022
8047
  * resolved blob back is a no-op for every unchanged secret.
8023
8048
  */
8024
8049
  async resolveGlobalStore(nodeId, cap) {
8050
+ await this.loadScopedSchemas(cap);
8025
8051
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8026
8052
  return this.projectForNode(raw, nodeId, cap);
8027
8053
  }
@@ -8032,6 +8058,7 @@ var BaseAddon = class {
8032
8058
  }
8033
8059
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8034
8060
  async readRawProjectedStore(nodeId, cap) {
8061
+ await this.loadScopedSchemas(cap);
8035
8062
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8036
8063
  return this.projectForNode(raw, nodeId, cap);
8037
8064
  }
@@ -8134,6 +8161,7 @@ var BaseAddon = class {
8134
8161
  await this.applyGlobalPatch(patch, nodeId);
8135
8162
  }
8136
8163
  async applyGlobalPatch(barePatch, nodeId) {
8164
+ await this.loadScopedSchemas();
8137
8165
  const keys = this.perNodeKeys();
8138
8166
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8139
8167
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8147,7 +8175,8 @@ var BaseAddon = class {
8147
8175
  await this.resolveConfig();
8148
8176
  await this.onConfigChanged();
8149
8177
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8150
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8178
+ const legacy = this.globalSettingsSchema();
8179
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8151
8180
  }
8152
8181
  /**
8153
8182
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8155,14 +8184,67 @@ var BaseAddon = class {
8155
8184
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8156
8185
  * settings API behaves exactly like the legacy node-agnostic one.
8157
8186
  */
8158
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8187
+ /**
8188
+ * Load the scoped schemas of every capability this addon declares (the override and the
8189
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8190
+ * derived from the previous set. Idempotent.
8191
+ */
8192
+ async loadScopedSchemas(extraCap) {
8193
+ const caps = new Set([
8194
+ ...this.scopedSettingsCapabilities(),
8195
+ ...this._registeredCapNames,
8196
+ ...extraCap === void 0 ? [] : [extraCap]
8197
+ ]);
8198
+ let added = false;
8199
+ for (const cap of caps) {
8200
+ if (this._scopedByCap.has(cap)) continue;
8201
+ const loaded = [];
8202
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8203
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8204
+ if (schema !== null) loaded.push({
8205
+ scope,
8206
+ schema
8207
+ });
8208
+ }
8209
+ this._scopedByCap.set(cap, loaded);
8210
+ added = true;
8211
+ }
8212
+ if (!added) return;
8213
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8214
+ this._addonSecretTree = null;
8215
+ }
8216
+ /**
8217
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8218
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8219
+ */
8220
+ scopedFormSchema(cap) {
8221
+ const byId = /* @__PURE__ */ new Map();
8222
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8223
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8224
+ }
8225
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8226
+ loadedScopedSchemas(cap) {
8227
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8228
+ return [...this._scopedByCap.values()].flat();
8229
+ }
8230
+ /**
8231
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8232
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8233
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8234
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8235
+ * Call {@link loadScopedSchemas} first (the async seams do).
8236
+ */
8159
8237
  perNodeKeys(cap) {
8160
8238
  const cacheKey = cap ?? "";
8161
- const cached = this._perNodeKeysCache.get(cacheKey);
8239
+ const cached = this._nodeKeysByCap.get(cacheKey);
8162
8240
  if (cached) return cached;
8163
- const schema = this.globalSettingsSchema(cap);
8164
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8165
- this._perNodeKeysCache.set(cacheKey, keys);
8241
+ const legacy = this.globalSettingsSchema(cap);
8242
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8243
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8244
+ if (scope !== "node") continue;
8245
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8246
+ }
8247
+ this._nodeKeysByCap.set(cacheKey, keys);
8166
8248
  return keys;
8167
8249
  }
8168
8250
  /**
@@ -8172,10 +8254,9 @@ var BaseAddon = class {
8172
8254
  * time to return its response before the addon is torn down and
8173
8255
  * re-initialised by `AddonRegistryService.restartAddon`.
8174
8256
  */
8175
- maybeAutoRestart(patchKeys, schema) {
8176
- if (!schema) return;
8257
+ maybeAutoRestart(patchKeys, schemas) {
8177
8258
  const restartKeys = /* @__PURE__ */ new Set();
8178
- for (const section of schema.sections) for (const field of section.fields) {
8259
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8179
8260
  if (field.type === "separator" || field.type === "info") continue;
8180
8261
  if (field.requiresRestart) restartKeys.add(field.key);
8181
8262
  }
@@ -8371,12 +8452,14 @@ var BaseAddon = class {
8371
8452
  * here, in the addon's own process, so the hub keeps no copy of it.
8372
8453
  */
8373
8454
  async getDeclaredSettingsKeys() {
8455
+ await this.loadScopedSchemas();
8374
8456
  return { keys: [...this.declaredKeySet()].toSorted() };
8375
8457
  }
8376
8458
  declaredKeySet() {
8377
8459
  const keys = new Set(Object.keys(this.defaults));
8378
8460
  const schema = this.globalSettingsSchema();
8379
8461
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8462
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8380
8463
  for (const key of this._declaredStateKeys) keys.add(key);
8381
8464
  for (const path of this.secretStatePaths) {
8382
8465
  const root = path.split(/[.[]/, 1)[0];
@@ -8472,7 +8555,11 @@ var BaseAddon = class {
8472
8555
  }
8473
8556
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8474
8557
  addonSecretTree() {
8475
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8558
+ if (this._addonSecretTree === null) {
8559
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8560
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8561
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8562
+ }
8476
8563
  return this._addonSecretTree;
8477
8564
  }
8478
8565
  deviceSecretTree() {
@@ -8722,6 +8809,23 @@ function dropPerNodeFields(fields) {
8722
8809
  }
8723
8810
  return kept;
8724
8811
  }
8812
+ /** The keys of the value-holding fields, recursing into layout containers. */
8813
+ function collectValueFieldKeys(fields) {
8814
+ const collected = [];
8815
+ for (const field of fields) {
8816
+ if (field.type === "group") {
8817
+ collected.push(...collectValueFieldKeys(field.fields));
8818
+ continue;
8819
+ }
8820
+ if (field.type === "sub-tabs") {
8821
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8822
+ continue;
8823
+ }
8824
+ if (field.type === "separator" || field.type === "info") continue;
8825
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8826
+ }
8827
+ return collected;
8828
+ }
8725
8829
  function collectFieldKeys(fields) {
8726
8830
  const collected = [];
8727
8831
  for (const field of fields) {
@@ -11934,7 +12038,7 @@ object({
11934
12038
  params: record(string(), union([string(), number()])).optional()
11935
12039
  });
11936
12040
  //#endregion
11937
- //#region ../types/dist/composition-CRNwcFYY.mjs
12041
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11938
12042
  /**
11939
12043
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11940
12044
  *
@@ -13406,12 +13510,22 @@ var RecordIntegrationInstances = class {
13406
13510
  ...this.hooks.description,
13407
13511
  mintsId: true
13408
13512
  };
13409
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13513
+ /**
13514
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13515
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13516
+ * record id answers as it is. An id that names nothing comes back unchanged.
13517
+ */
13518
+ async resolveRecordId(id) {
13519
+ if (await this.handle.get(id) !== null) return id;
13520
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13521
+ return linked === void 0 ? id : linked.id;
13522
+ }
13523
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13410
13524
  listInstances = async () => {
13411
13525
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13412
13526
  };
13413
13527
  getInstance = async ({ id }) => {
13414
- const record = await this.handle.get(id);
13528
+ const record = await this.handle.get(await this.resolveRecordId(id));
13415
13529
  return record === null ? null : this.viewOf(record);
13416
13530
  };
13417
13531
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13428,7 +13542,8 @@ var RecordIntegrationInstances = class {
13428
13542
  await this.hooks.onChanged?.(minted, "created");
13429
13543
  return { id: minted };
13430
13544
  };
13431
- updateInstance = async ({ id, name, values }) => {
13545
+ updateInstance = async ({ id: askedId, name, values }) => {
13546
+ const id = await this.resolveRecordId(askedId);
13432
13547
  const patch = {};
13433
13548
  for (const [key, value] of Object.entries(values)) {
13434
13549
  if (value === void 0) continue;
@@ -13463,7 +13578,10 @@ var RecordIntegrationInstances = class {
13463
13578
  status: "unsupported",
13464
13579
  error: "This integration declares no connection test"
13465
13580
  };
13466
- return this.hooks.test(input);
13581
+ return this.hooks.test(input.id === null ? input : {
13582
+ ...input,
13583
+ id: await this.resolveRecordId(input.id)
13584
+ });
13467
13585
  };
13468
13586
  /** The flat provider object to hand to `registerProvider`. */
13469
13587
  toProvider() {
@@ -14642,20 +14760,28 @@ async function moveIntegrationRowsToRecords(ports, spec) {
14642
14760
  };
14643
14761
  }
14644
14762
  /**
14645
- * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
14646
- * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
14647
- * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
14648
- * the new record through the addon's own records handle. The hub deletes the rows and their
14649
- * keychain values; the addon never touches the registry.
14650
- */
14763
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416). An addon runs the move
14764
+ * from its own process, so the leftover rows are reached through the migration-only door
14765
+ * (`migrations.readLegacyIntegrationRows` / `clearLegacyIntegrationRows`) on the hub's connection.
14766
+ * The door returns markers. This process is the owner, so it resolves and releases them with
14767
+ * `resolveSecretMarkers` — the same call the old settings door used — and only then asks the
14768
+ * door to delete the rows. The addon's own record kind is its records handle.
14769
+ */
14770
+ async function resolvedRows(stored, secrets) {
14771
+ if (secrets === null) return stored;
14772
+ const { value, unresolved } = await resolveSecretMarkers(stored, secrets);
14773
+ if (unresolved.length > 0) throw new Error(`${unresolved.length} secret(s) cannot be read from the keychain`);
14774
+ return value;
14775
+ }
14651
14776
  function integrationRowsPortsOverApi(deps) {
14777
+ const secrets = deps.secrets ?? null;
14652
14778
  return {
14653
14779
  warn: (message) => deps.log.warn(message),
14654
14780
  listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
14655
14781
  id: i.id,
14656
14782
  name: i.name
14657
14783
  })),
14658
- readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
14784
+ readRows: async (integrationId) => resolvedRows(await deps.api.readLegacyIntegrationRows({ integrationId }), secrets),
14659
14785
  putRecord: async (id, body) => {
14660
14786
  if (await deps.records.get(id) !== null) return "exists";
14661
14787
  try {
@@ -14674,7 +14800,9 @@ function integrationRowsPortsOverApi(deps) {
14674
14800
  await deps.records.delete(id);
14675
14801
  },
14676
14802
  clearRows: async (integrationId) => {
14677
- await deps.api.clearSettings({ id: integrationId });
14803
+ const refs = collectSecretMarkerRefs(await deps.api.readLegacyIntegrationRows({ integrationId }));
14804
+ if (refs.length > 0 && secrets !== null) await secrets.delete(refs);
14805
+ await deps.api.clearLegacyIntegrationRows({ integrationId });
14678
14806
  }
14679
14807
  };
14680
14808
  }
@@ -14742,6 +14870,7 @@ function integrationRowsMigration(spec) {
14742
14870
  function integrationRowsPortsOver(deps) {
14743
14871
  return integrationRowsPortsOverApi({
14744
14872
  api: deps.api,
14873
+ ...deps.secrets !== void 0 ? { secrets: deps.secrets } : {},
14745
14874
  records: deps.records.kind({
14746
14875
  name: deps.kind.name,
14747
14876
  declaration: deps.kind.declaration,
@@ -14753,8 +14882,8 @@ function integrationRowsPortsOver(deps) {
14753
14882
  function defaultIntegrationApiOver(api) {
14754
14883
  return {
14755
14884
  list: () => api.integrations.list.query(),
14756
- getSettings: (input) => api.integrations.getSettings.query(input),
14757
- clearSettings: (input) => api.integrations.clearSettings.mutate(input),
14885
+ readLegacyIntegrationRows: (input) => api.migrations.readLegacyIntegrationRows.query(input),
14886
+ clearLegacyIntegrationRows: (input) => api.migrations.clearLegacyIntegrationRows.mutate(input),
14758
14887
  listDevices: (addonId) => api.deviceManager.listAll.query({
14759
14888
  addonId,
14760
14889
  projection: "slim"
@@ -50432,6 +50561,9 @@ var RunningProposalEvidenceSchema = object({
50432
50561
  runsFound: number().int().nonnegative(),
50433
50562
  pausesFound: number().int().nonnegative(),
50434
50563
  longestPauseMs: number().nonnegative(),
50564
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50565
+ pulsesFound: number().int().nonnegative(),
50566
+ longestPulseMs: number().nonnegative(),
50435
50567
  idleFloorW: number(),
50436
50568
  noiseCeilingW: number(),
50437
50569
  plateauW: number(),
@@ -56298,11 +56430,7 @@ var CreateIntegrationInputSchema = object({
56298
56430
  info: record(string(), unknown()).optional(),
56299
56431
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56300
56432
  originStableId: string().optional(),
56301
- settings: record(string(), unknown()).optional(),
56302
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56303
- * the keychain, not the table (D787). */
56304
- secretKeys: array(string()).optional(),
56305
- skipRestart: boolean().optional()
56433
+ settings: record(string(), unknown()).optional()
56306
56434
  });
56307
56435
  var UpdateIntegrationInputSchema = object({
56308
56436
  id: string(),
@@ -56323,22 +56451,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56323
56451
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
56324
56452
  kind: "mutation",
56325
56453
  auth: "admin"
56326
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
56327
- id: string(),
56328
- settings: record(string(), unknown()),
56329
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56330
- secretKeys: array(string()).optional(),
56331
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56332
- skipRestart: boolean().optional()
56333
- }), object({ success: boolean() }), {
56334
- kind: "mutation",
56335
- auth: "admin"
56336
- }), method(object({ id: string() }), object({
56337
- rows: number().int().nonnegative(),
56338
- secrets: number().int().nonnegative()
56339
- }), {
56340
- kind: "mutation",
56341
- auth: "admin"
56342
56454
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56343
56455
  addonId: string(),
56344
56456
  settings: record(string(), unknown())
@@ -59315,6 +59427,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59315
59427
  }), {
59316
59428
  kind: "mutation",
59317
59429
  auth: "admin"
59430
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
59431
+ rows: number().int().nonnegative(),
59432
+ secrets: number().int().nonnegative()
59433
+ }), {
59434
+ kind: "mutation",
59435
+ auth: "admin"
59436
+ }), method(object({
59437
+ integrationId: string().min(1),
59438
+ patch: record(string(), unknown())
59439
+ }), object({ written: boolean() }), {
59440
+ kind: "mutation",
59441
+ auth: "admin"
59442
+ }), method(object({
59443
+ integrationId: string().min(1),
59444
+ keys: array(string().min(1))
59445
+ }), object({
59446
+ present: boolean(),
59447
+ removed: record(string(), unknown())
59448
+ }), {
59449
+ kind: "mutation",
59450
+ auth: "admin"
59318
59451
  });
59319
59452
  method(object({
59320
59453
  sourceUrl: string(),
@@ -80733,11 +80866,15 @@ discriminatedUnion("kind", [object({
80733
80866
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80734
80867
  * near F. A device that is not near one level most of the time has no floor.
80735
80868
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80736
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80737
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80869
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80870
+ * display waking): it is neither a run nor a pause. Among the remaining
80871
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80872
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80873
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80874
+ * is no run.
80738
80875
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80739
- * grid a person would type; stop dwell covers the longest pause; start dwell
80740
- * covers the longest standby blip above T.
80876
+ * grid a person would type; stop dwell covers the longest pause and the
80877
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80741
80878
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80742
80879
  * run must hold exactly one true interval and nothing may flap outside.
80743
80880
  */
@@ -80748,6 +80885,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80748
80885
  30 * SECOND_MS;
80749
80886
  60 * SECOND_MS;
80750
80887
  20 * MINUTE_MS$1;
80888
+ 90 * SECOND_MS;
80889
+ 30 * MINUTE_MS$1;
80890
+ 10 * MINUTE_MS$1;
80891
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80751
80892
  10 * SECOND_MS;
80752
80893
  10 * SECOND_MS;
80753
80894
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -86032,13 +86173,6 @@ Object.freeze({
86032
86173
  access: "create",
86033
86174
  role: "admin"
86034
86175
  },
86035
- "integrations.clearSettings": {
86036
- capName: "integrations",
86037
- capScope: "system",
86038
- addonId: null,
86039
- access: "delete",
86040
- role: "admin"
86041
- },
86042
86176
  "integrations.create": {
86043
86177
  capName: "integrations",
86044
86178
  capScope: "system",
@@ -86074,13 +86208,6 @@ Object.freeze({
86074
86208
  access: "view",
86075
86209
  role: "admin"
86076
86210
  },
86077
- "integrations.getSettings": {
86078
- capName: "integrations",
86079
- capScope: "system",
86080
- addonId: null,
86081
- access: "view",
86082
- role: "admin"
86083
- },
86084
86211
  "integrations.list": {
86085
86212
  capName: "integrations",
86086
86213
  capScope: "system",
@@ -86088,13 +86215,6 @@ Object.freeze({
86088
86215
  access: "view",
86089
86216
  role: "admin"
86090
86217
  },
86091
- "integrations.setSettings": {
86092
- capName: "integrations",
86093
- capScope: "system",
86094
- addonId: null,
86095
- access: "create",
86096
- role: "admin"
86097
- },
86098
86218
  "integrations.testConnection": {
86099
86219
  capName: "integrations",
86100
86220
  capScope: "system",
@@ -87089,6 +87209,13 @@ Object.freeze({
87089
87209
  access: "view",
87090
87210
  role: "admin"
87091
87211
  },
87212
+ "migrations.clearLegacyIntegrationRows": {
87213
+ capName: "migrations",
87214
+ capScope: "system",
87215
+ addonId: null,
87216
+ access: "delete",
87217
+ role: "admin"
87218
+ },
87092
87219
  "migrations.declareOwner": {
87093
87220
  capName: "migrations",
87094
87221
  capScope: "system",
@@ -87096,6 +87223,13 @@ Object.freeze({
87096
87223
  access: "create",
87097
87224
  role: "admin"
87098
87225
  },
87226
+ "migrations.deleteLegacyIntegrationRowKeys": {
87227
+ capName: "migrations",
87228
+ capScope: "system",
87229
+ addonId: null,
87230
+ access: "delete",
87231
+ role: "admin"
87232
+ },
87099
87233
  "migrations.dryRun": {
87100
87234
  capName: "migrations",
87101
87235
  capScope: "system",
@@ -87131,6 +87265,20 @@ Object.freeze({
87131
87265
  access: "create",
87132
87266
  role: "admin"
87133
87267
  },
87268
+ "migrations.patchLegacyIntegrationRows": {
87269
+ capName: "migrations",
87270
+ capScope: "system",
87271
+ addonId: null,
87272
+ access: "create",
87273
+ role: "admin"
87274
+ },
87275
+ "migrations.readLegacyIntegrationRows": {
87276
+ capName: "migrations",
87277
+ capScope: "system",
87278
+ addonId: null,
87279
+ access: "view",
87280
+ role: "admin"
87281
+ },
87134
87282
  "migrations.restorePreMigration": {
87135
87283
  capName: "migrations",
87136
87284
  capScope: "system",
@@ -97527,7 +97675,7 @@ function connectionFromSettings(settings, tokenLocked) {
97527
97675
  function sameConnection(a, b) {
97528
97676
  return JSON.stringify(a) === JSON.stringify(b);
97529
97677
  }
97530
- /** The integration's settings form (`globalSettingsSchema`); the integration carries the name. */
97678
+ /** The integration instance form (the `formSchema` of its instances); the integration carries the name. */
97531
97679
  function buildConnectionFormSchema() {
97532
97680
  return { sections: [
97533
97681
  {
@@ -99232,6 +99380,7 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
99232
99380
  fixtureMark: "host"
99233
99381
  })], integrationRowsPortsOver({
99234
99382
  api: defaultIntegrationApiOver(this.ctx.api),
99383
+ secrets: this.ctx.secrets ?? null,
99235
99384
  records,
99236
99385
  kind: enpalConnectionsEntryKind,
99237
99386
  warn: (message) => this.ctx.logger.warn(message)
package/dist/addon.mjs CHANGED
@@ -5980,7 +5980,7 @@ var ZodIssueCode = {
5980
5980
  var ZodFirstPartyTypeKind;
5981
5981
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5982
5982
  //#endregion
5983
- //#region ../types/dist/sleep-D1jjWAr8.mjs
5983
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
5984
5984
  /**
5985
5985
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5986
5986
  * window to float samples (D455).
@@ -6789,6 +6789,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6789
6789
  }
6790
6790
  return out;
6791
6791
  }
6792
+ /** Every scope, in the order the guard and the docs list them. */
6793
+ var SCOPED_SETTINGS_SCOPES = [
6794
+ "cluster",
6795
+ "device-defaults",
6796
+ "node"
6797
+ ];
6792
6798
  /**
6793
6799
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6794
6800
  *
@@ -7680,6 +7686,13 @@ var BaseAddon = class {
7680
7686
  */
7681
7687
  secretStatePaths = [];
7682
7688
  _addonSecretTree = null;
7689
+ /**
7690
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7691
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7692
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7693
+ */
7694
+ _scopedByCap = /* @__PURE__ */ new Map();
7695
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7683
7696
  _deviceSecretTree = null;
7684
7697
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7685
7698
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7749,6 +7762,7 @@ var BaseAddon = class {
7749
7762
  this._ctx = context;
7750
7763
  this._secretSettings = null;
7751
7764
  }
7765
+ await this.loadScopedSchemas();
7752
7766
  await this.resolveConfig();
7753
7767
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7754
7768
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7758,6 +7772,7 @@ var BaseAddon = class {
7758
7772
  const normalized = normalizeAddonInitResult(result);
7759
7773
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7760
7774
  this._registeredCapNames = providers.map((p) => p.capability.name);
7775
+ await this.loadScopedSchemas();
7761
7776
  return normalized;
7762
7777
  }
7763
7778
  /**
@@ -7846,6 +7861,16 @@ var BaseAddon = class {
7846
7861
  schema(schema) {
7847
7862
  return schema;
7848
7863
  }
7864
+ /**
7865
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7866
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7867
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7868
+ * must already know which keys are per node. Names the providers register at init are added
7869
+ * once known.
7870
+ */
7871
+ scopedSettingsCapabilities() {
7872
+ return [];
7873
+ }
7849
7874
  /** Override to provide global-level settings UI schema. */
7850
7875
  globalSettingsSchema(_cap) {
7851
7876
  return null;
@@ -7889,7 +7914,8 @@ var BaseAddon = class {
7889
7914
  return [];
7890
7915
  }
7891
7916
  async getGlobalSettings(overlay, cap, nodeId) {
7892
- const schema = this.globalSettingsSchema(cap);
7917
+ await this.loadScopedSchemas(cap);
7918
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7893
7919
  if (!schema) return { sections: [] };
7894
7920
  const projected = await this.readRawProjectedStore(nodeId, cap);
7895
7921
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7924,10 +7950,9 @@ var BaseAddon = class {
7924
7950
  *
7925
7951
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7926
7952
  * needs its connection fields, the default integration the addon's own sections. It is the
7927
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7928
- * unchanged (`integrations.setSettings` for an account integration).
7953
+ * SHAPE of the form only — the values live on the instance record.
7929
7954
  */
7930
- async integrationSettingsSchema(_instance) {
7955
+ async integrationFormSchema(_instance) {
7931
7956
  return null;
7932
7957
  }
7933
7958
  /**
@@ -7973,7 +7998,7 @@ var BaseAddon = class {
7973
7998
  * per-instance schema when it declares one, else its global schema (what they always showed).
7974
7999
  */
7975
8000
  async getIntegrationFormSchema(instance, nodeId) {
7976
- const scoped = await this.integrationSettingsSchema(instance);
8001
+ const scoped = await this.integrationFormSchema(instance);
7977
8002
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7978
8003
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7979
8004
  }
@@ -8021,6 +8046,7 @@ var BaseAddon = class {
8021
8046
  * resolved blob back is a no-op for every unchanged secret.
8022
8047
  */
8023
8048
  async resolveGlobalStore(nodeId, cap) {
8049
+ await this.loadScopedSchemas(cap);
8024
8050
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8025
8051
  return this.projectForNode(raw, nodeId, cap);
8026
8052
  }
@@ -8031,6 +8057,7 @@ var BaseAddon = class {
8031
8057
  }
8032
8058
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8033
8059
  async readRawProjectedStore(nodeId, cap) {
8060
+ await this.loadScopedSchemas(cap);
8034
8061
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8035
8062
  return this.projectForNode(raw, nodeId, cap);
8036
8063
  }
@@ -8133,6 +8160,7 @@ var BaseAddon = class {
8133
8160
  await this.applyGlobalPatch(patch, nodeId);
8134
8161
  }
8135
8162
  async applyGlobalPatch(barePatch, nodeId) {
8163
+ await this.loadScopedSchemas();
8136
8164
  const keys = this.perNodeKeys();
8137
8165
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8138
8166
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8146,7 +8174,8 @@ var BaseAddon = class {
8146
8174
  await this.resolveConfig();
8147
8175
  await this.onConfigChanged();
8148
8176
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8149
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8177
+ const legacy = this.globalSettingsSchema();
8178
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8150
8179
  }
8151
8180
  /**
8152
8181
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8154,14 +8183,67 @@ var BaseAddon = class {
8154
8183
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8155
8184
  * settings API behaves exactly like the legacy node-agnostic one.
8156
8185
  */
8157
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8186
+ /**
8187
+ * Load the scoped schemas of every capability this addon declares (the override and the
8188
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8189
+ * derived from the previous set. Idempotent.
8190
+ */
8191
+ async loadScopedSchemas(extraCap) {
8192
+ const caps = new Set([
8193
+ ...this.scopedSettingsCapabilities(),
8194
+ ...this._registeredCapNames,
8195
+ ...extraCap === void 0 ? [] : [extraCap]
8196
+ ]);
8197
+ let added = false;
8198
+ for (const cap of caps) {
8199
+ if (this._scopedByCap.has(cap)) continue;
8200
+ const loaded = [];
8201
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8202
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8203
+ if (schema !== null) loaded.push({
8204
+ scope,
8205
+ schema
8206
+ });
8207
+ }
8208
+ this._scopedByCap.set(cap, loaded);
8209
+ added = true;
8210
+ }
8211
+ if (!added) return;
8212
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8213
+ this._addonSecretTree = null;
8214
+ }
8215
+ /**
8216
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8217
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8218
+ */
8219
+ scopedFormSchema(cap) {
8220
+ const byId = /* @__PURE__ */ new Map();
8221
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8222
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8223
+ }
8224
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8225
+ loadedScopedSchemas(cap) {
8226
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8227
+ return [...this._scopedByCap.values()].flat();
8228
+ }
8229
+ /**
8230
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8231
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8232
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8233
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8234
+ * Call {@link loadScopedSchemas} first (the async seams do).
8235
+ */
8158
8236
  perNodeKeys(cap) {
8159
8237
  const cacheKey = cap ?? "";
8160
- const cached = this._perNodeKeysCache.get(cacheKey);
8238
+ const cached = this._nodeKeysByCap.get(cacheKey);
8161
8239
  if (cached) return cached;
8162
- const schema = this.globalSettingsSchema(cap);
8163
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8164
- this._perNodeKeysCache.set(cacheKey, keys);
8240
+ const legacy = this.globalSettingsSchema(cap);
8241
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8242
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8243
+ if (scope !== "node") continue;
8244
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8245
+ }
8246
+ this._nodeKeysByCap.set(cacheKey, keys);
8165
8247
  return keys;
8166
8248
  }
8167
8249
  /**
@@ -8171,10 +8253,9 @@ var BaseAddon = class {
8171
8253
  * time to return its response before the addon is torn down and
8172
8254
  * re-initialised by `AddonRegistryService.restartAddon`.
8173
8255
  */
8174
- maybeAutoRestart(patchKeys, schema) {
8175
- if (!schema) return;
8256
+ maybeAutoRestart(patchKeys, schemas) {
8176
8257
  const restartKeys = /* @__PURE__ */ new Set();
8177
- for (const section of schema.sections) for (const field of section.fields) {
8258
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8178
8259
  if (field.type === "separator" || field.type === "info") continue;
8179
8260
  if (field.requiresRestart) restartKeys.add(field.key);
8180
8261
  }
@@ -8370,12 +8451,14 @@ var BaseAddon = class {
8370
8451
  * here, in the addon's own process, so the hub keeps no copy of it.
8371
8452
  */
8372
8453
  async getDeclaredSettingsKeys() {
8454
+ await this.loadScopedSchemas();
8373
8455
  return { keys: [...this.declaredKeySet()].toSorted() };
8374
8456
  }
8375
8457
  declaredKeySet() {
8376
8458
  const keys = new Set(Object.keys(this.defaults));
8377
8459
  const schema = this.globalSettingsSchema();
8378
8460
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8461
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8379
8462
  for (const key of this._declaredStateKeys) keys.add(key);
8380
8463
  for (const path of this.secretStatePaths) {
8381
8464
  const root = path.split(/[.[]/, 1)[0];
@@ -8471,7 +8554,11 @@ var BaseAddon = class {
8471
8554
  }
8472
8555
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8473
8556
  addonSecretTree() {
8474
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8557
+ if (this._addonSecretTree === null) {
8558
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8559
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8560
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8561
+ }
8475
8562
  return this._addonSecretTree;
8476
8563
  }
8477
8564
  deviceSecretTree() {
@@ -8721,6 +8808,23 @@ function dropPerNodeFields(fields) {
8721
8808
  }
8722
8809
  return kept;
8723
8810
  }
8811
+ /** The keys of the value-holding fields, recursing into layout containers. */
8812
+ function collectValueFieldKeys(fields) {
8813
+ const collected = [];
8814
+ for (const field of fields) {
8815
+ if (field.type === "group") {
8816
+ collected.push(...collectValueFieldKeys(field.fields));
8817
+ continue;
8818
+ }
8819
+ if (field.type === "sub-tabs") {
8820
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8821
+ continue;
8822
+ }
8823
+ if (field.type === "separator" || field.type === "info") continue;
8824
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8825
+ }
8826
+ return collected;
8827
+ }
8724
8828
  function collectFieldKeys(fields) {
8725
8829
  const collected = [];
8726
8830
  for (const field of fields) {
@@ -11933,7 +12037,7 @@ object({
11933
12037
  params: record(string(), union([string(), number()])).optional()
11934
12038
  });
11935
12039
  //#endregion
11936
- //#region ../types/dist/composition-CRNwcFYY.mjs
12040
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11937
12041
  /**
11938
12042
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11939
12043
  *
@@ -13405,12 +13509,22 @@ var RecordIntegrationInstances = class {
13405
13509
  ...this.hooks.description,
13406
13510
  mintsId: true
13407
13511
  };
13408
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13512
+ /**
13513
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13514
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13515
+ * record id answers as it is. An id that names nothing comes back unchanged.
13516
+ */
13517
+ async resolveRecordId(id) {
13518
+ if (await this.handle.get(id) !== null) return id;
13519
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13520
+ return linked === void 0 ? id : linked.id;
13521
+ }
13522
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13409
13523
  listInstances = async () => {
13410
13524
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13411
13525
  };
13412
13526
  getInstance = async ({ id }) => {
13413
- const record = await this.handle.get(id);
13527
+ const record = await this.handle.get(await this.resolveRecordId(id));
13414
13528
  return record === null ? null : this.viewOf(record);
13415
13529
  };
13416
13530
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13427,7 +13541,8 @@ var RecordIntegrationInstances = class {
13427
13541
  await this.hooks.onChanged?.(minted, "created");
13428
13542
  return { id: minted };
13429
13543
  };
13430
- updateInstance = async ({ id, name, values }) => {
13544
+ updateInstance = async ({ id: askedId, name, values }) => {
13545
+ const id = await this.resolveRecordId(askedId);
13431
13546
  const patch = {};
13432
13547
  for (const [key, value] of Object.entries(values)) {
13433
13548
  if (value === void 0) continue;
@@ -13462,7 +13577,10 @@ var RecordIntegrationInstances = class {
13462
13577
  status: "unsupported",
13463
13578
  error: "This integration declares no connection test"
13464
13579
  };
13465
- return this.hooks.test(input);
13580
+ return this.hooks.test(input.id === null ? input : {
13581
+ ...input,
13582
+ id: await this.resolveRecordId(input.id)
13583
+ });
13466
13584
  };
13467
13585
  /** The flat provider object to hand to `registerProvider`. */
13468
13586
  toProvider() {
@@ -14641,20 +14759,28 @@ async function moveIntegrationRowsToRecords(ports, spec) {
14641
14759
  };
14642
14760
  }
14643
14761
  /**
14644
- * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
14645
- * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
14646
- * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
14647
- * the new record through the addon's own records handle. The hub deletes the rows and their
14648
- * keychain values; the addon never touches the registry.
14649
- */
14762
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416). An addon runs the move
14763
+ * from its own process, so the leftover rows are reached through the migration-only door
14764
+ * (`migrations.readLegacyIntegrationRows` / `clearLegacyIntegrationRows`) on the hub's connection.
14765
+ * The door returns markers. This process is the owner, so it resolves and releases them with
14766
+ * `resolveSecretMarkers` — the same call the old settings door used — and only then asks the
14767
+ * door to delete the rows. The addon's own record kind is its records handle.
14768
+ */
14769
+ async function resolvedRows(stored, secrets) {
14770
+ if (secrets === null) return stored;
14771
+ const { value, unresolved } = await resolveSecretMarkers(stored, secrets);
14772
+ if (unresolved.length > 0) throw new Error(`${unresolved.length} secret(s) cannot be read from the keychain`);
14773
+ return value;
14774
+ }
14650
14775
  function integrationRowsPortsOverApi(deps) {
14776
+ const secrets = deps.secrets ?? null;
14651
14777
  return {
14652
14778
  warn: (message) => deps.log.warn(message),
14653
14779
  listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
14654
14780
  id: i.id,
14655
14781
  name: i.name
14656
14782
  })),
14657
- readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
14783
+ readRows: async (integrationId) => resolvedRows(await deps.api.readLegacyIntegrationRows({ integrationId }), secrets),
14658
14784
  putRecord: async (id, body) => {
14659
14785
  if (await deps.records.get(id) !== null) return "exists";
14660
14786
  try {
@@ -14673,7 +14799,9 @@ function integrationRowsPortsOverApi(deps) {
14673
14799
  await deps.records.delete(id);
14674
14800
  },
14675
14801
  clearRows: async (integrationId) => {
14676
- await deps.api.clearSettings({ id: integrationId });
14802
+ const refs = collectSecretMarkerRefs(await deps.api.readLegacyIntegrationRows({ integrationId }));
14803
+ if (refs.length > 0 && secrets !== null) await secrets.delete(refs);
14804
+ await deps.api.clearLegacyIntegrationRows({ integrationId });
14677
14805
  }
14678
14806
  };
14679
14807
  }
@@ -14741,6 +14869,7 @@ function integrationRowsMigration(spec) {
14741
14869
  function integrationRowsPortsOver(deps) {
14742
14870
  return integrationRowsPortsOverApi({
14743
14871
  api: deps.api,
14872
+ ...deps.secrets !== void 0 ? { secrets: deps.secrets } : {},
14744
14873
  records: deps.records.kind({
14745
14874
  name: deps.kind.name,
14746
14875
  declaration: deps.kind.declaration,
@@ -14752,8 +14881,8 @@ function integrationRowsPortsOver(deps) {
14752
14881
  function defaultIntegrationApiOver(api) {
14753
14882
  return {
14754
14883
  list: () => api.integrations.list.query(),
14755
- getSettings: (input) => api.integrations.getSettings.query(input),
14756
- clearSettings: (input) => api.integrations.clearSettings.mutate(input),
14884
+ readLegacyIntegrationRows: (input) => api.migrations.readLegacyIntegrationRows.query(input),
14885
+ clearLegacyIntegrationRows: (input) => api.migrations.clearLegacyIntegrationRows.mutate(input),
14757
14886
  listDevices: (addonId) => api.deviceManager.listAll.query({
14758
14887
  addonId,
14759
14888
  projection: "slim"
@@ -50431,6 +50560,9 @@ var RunningProposalEvidenceSchema = object({
50431
50560
  runsFound: number().int().nonnegative(),
50432
50561
  pausesFound: number().int().nonnegative(),
50433
50562
  longestPauseMs: number().nonnegative(),
50563
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50564
+ pulsesFound: number().int().nonnegative(),
50565
+ longestPulseMs: number().nonnegative(),
50434
50566
  idleFloorW: number(),
50435
50567
  noiseCeilingW: number(),
50436
50568
  plateauW: number(),
@@ -56297,11 +56429,7 @@ var CreateIntegrationInputSchema = object({
56297
56429
  info: record(string(), unknown()).optional(),
56298
56430
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56299
56431
  originStableId: string().optional(),
56300
- settings: record(string(), unknown()).optional(),
56301
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56302
- * the keychain, not the table (D787). */
56303
- secretKeys: array(string()).optional(),
56304
- skipRestart: boolean().optional()
56432
+ settings: record(string(), unknown()).optional()
56305
56433
  });
56306
56434
  var UpdateIntegrationInputSchema = object({
56307
56435
  id: string(),
@@ -56322,22 +56450,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56322
56450
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
56323
56451
  kind: "mutation",
56324
56452
  auth: "admin"
56325
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
56326
- id: string(),
56327
- settings: record(string(), unknown()),
56328
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56329
- secretKeys: array(string()).optional(),
56330
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56331
- skipRestart: boolean().optional()
56332
- }), object({ success: boolean() }), {
56333
- kind: "mutation",
56334
- auth: "admin"
56335
- }), method(object({ id: string() }), object({
56336
- rows: number().int().nonnegative(),
56337
- secrets: number().int().nonnegative()
56338
- }), {
56339
- kind: "mutation",
56340
- auth: "admin"
56341
56453
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56342
56454
  addonId: string(),
56343
56455
  settings: record(string(), unknown())
@@ -59314,6 +59426,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59314
59426
  }), {
59315
59427
  kind: "mutation",
59316
59428
  auth: "admin"
59429
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
59430
+ rows: number().int().nonnegative(),
59431
+ secrets: number().int().nonnegative()
59432
+ }), {
59433
+ kind: "mutation",
59434
+ auth: "admin"
59435
+ }), method(object({
59436
+ integrationId: string().min(1),
59437
+ patch: record(string(), unknown())
59438
+ }), object({ written: boolean() }), {
59439
+ kind: "mutation",
59440
+ auth: "admin"
59441
+ }), method(object({
59442
+ integrationId: string().min(1),
59443
+ keys: array(string().min(1))
59444
+ }), object({
59445
+ present: boolean(),
59446
+ removed: record(string(), unknown())
59447
+ }), {
59448
+ kind: "mutation",
59449
+ auth: "admin"
59317
59450
  });
59318
59451
  method(object({
59319
59452
  sourceUrl: string(),
@@ -80732,11 +80865,15 @@ discriminatedUnion("kind", [object({
80732
80865
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80733
80866
  * near F. A device that is not near one level most of the time has no floor.
80734
80867
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80735
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80736
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80868
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80869
+ * display waking): it is neither a run nor a pause. Among the remaining
80870
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80871
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80872
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80873
+ * is no run.
80737
80874
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80738
- * grid a person would type; stop dwell covers the longest pause; start dwell
80739
- * covers the longest standby blip above T.
80875
+ * grid a person would type; stop dwell covers the longest pause and the
80876
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80740
80877
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80741
80878
  * run must hold exactly one true interval and nothing may flap outside.
80742
80879
  */
@@ -80747,6 +80884,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80747
80884
  30 * SECOND_MS;
80748
80885
  60 * SECOND_MS;
80749
80886
  20 * MINUTE_MS$1;
80887
+ 90 * SECOND_MS;
80888
+ 30 * MINUTE_MS$1;
80889
+ 10 * MINUTE_MS$1;
80890
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80750
80891
  10 * SECOND_MS;
80751
80892
  10 * SECOND_MS;
80752
80893
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -86031,13 +86172,6 @@ Object.freeze({
86031
86172
  access: "create",
86032
86173
  role: "admin"
86033
86174
  },
86034
- "integrations.clearSettings": {
86035
- capName: "integrations",
86036
- capScope: "system",
86037
- addonId: null,
86038
- access: "delete",
86039
- role: "admin"
86040
- },
86041
86175
  "integrations.create": {
86042
86176
  capName: "integrations",
86043
86177
  capScope: "system",
@@ -86073,13 +86207,6 @@ Object.freeze({
86073
86207
  access: "view",
86074
86208
  role: "admin"
86075
86209
  },
86076
- "integrations.getSettings": {
86077
- capName: "integrations",
86078
- capScope: "system",
86079
- addonId: null,
86080
- access: "view",
86081
- role: "admin"
86082
- },
86083
86210
  "integrations.list": {
86084
86211
  capName: "integrations",
86085
86212
  capScope: "system",
@@ -86087,13 +86214,6 @@ Object.freeze({
86087
86214
  access: "view",
86088
86215
  role: "admin"
86089
86216
  },
86090
- "integrations.setSettings": {
86091
- capName: "integrations",
86092
- capScope: "system",
86093
- addonId: null,
86094
- access: "create",
86095
- role: "admin"
86096
- },
86097
86217
  "integrations.testConnection": {
86098
86218
  capName: "integrations",
86099
86219
  capScope: "system",
@@ -87088,6 +87208,13 @@ Object.freeze({
87088
87208
  access: "view",
87089
87209
  role: "admin"
87090
87210
  },
87211
+ "migrations.clearLegacyIntegrationRows": {
87212
+ capName: "migrations",
87213
+ capScope: "system",
87214
+ addonId: null,
87215
+ access: "delete",
87216
+ role: "admin"
87217
+ },
87091
87218
  "migrations.declareOwner": {
87092
87219
  capName: "migrations",
87093
87220
  capScope: "system",
@@ -87095,6 +87222,13 @@ Object.freeze({
87095
87222
  access: "create",
87096
87223
  role: "admin"
87097
87224
  },
87225
+ "migrations.deleteLegacyIntegrationRowKeys": {
87226
+ capName: "migrations",
87227
+ capScope: "system",
87228
+ addonId: null,
87229
+ access: "delete",
87230
+ role: "admin"
87231
+ },
87098
87232
  "migrations.dryRun": {
87099
87233
  capName: "migrations",
87100
87234
  capScope: "system",
@@ -87130,6 +87264,20 @@ Object.freeze({
87130
87264
  access: "create",
87131
87265
  role: "admin"
87132
87266
  },
87267
+ "migrations.patchLegacyIntegrationRows": {
87268
+ capName: "migrations",
87269
+ capScope: "system",
87270
+ addonId: null,
87271
+ access: "create",
87272
+ role: "admin"
87273
+ },
87274
+ "migrations.readLegacyIntegrationRows": {
87275
+ capName: "migrations",
87276
+ capScope: "system",
87277
+ addonId: null,
87278
+ access: "view",
87279
+ role: "admin"
87280
+ },
87133
87281
  "migrations.restorePreMigration": {
87134
87282
  capName: "migrations",
87135
87283
  capScope: "system",
@@ -97526,7 +97674,7 @@ function connectionFromSettings(settings, tokenLocked) {
97526
97674
  function sameConnection(a, b) {
97527
97675
  return JSON.stringify(a) === JSON.stringify(b);
97528
97676
  }
97529
- /** The integration's settings form (`globalSettingsSchema`); the integration carries the name. */
97677
+ /** The integration instance form (the `formSchema` of its instances); the integration carries the name. */
97530
97678
  function buildConnectionFormSchema() {
97531
97679
  return { sections: [
97532
97680
  {
@@ -99231,6 +99379,7 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
99231
99379
  fixtureMark: "host"
99232
99380
  })], integrationRowsPortsOver({
99233
99381
  api: defaultIntegrationApiOver(this.ctx.api),
99382
+ secrets: this.ctx.secrets ?? null,
99234
99383
  records,
99235
99384
  kind: enpalConnectionsEntryKind,
99236
99385
  warn: (message) => this.ctx.logger.warn(message)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-enpal",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Enpal solar-box device-provider addon for CamStack — PV, battery, grid, house and heat-pump readings from the box's local InfluxDB (device page as fallback)",
5
5
  "keywords": [
6
6
  "camstack",
@@ -63,7 +63,7 @@
63
63
  }
64
64
  ],
65
65
  "requires": {
66
- "server": ">=1.2.765"
66
+ "server": ">=1.2.777"
67
67
  }
68
68
  },
69
69
  "files": [