@camstack/addon-provider-enpal 0.1.9 → 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,12 @@ 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
+
10
16
  ## [0.1.9] - 2026-11-05
11
17
 
12
18
  ### 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-9TIHFj1w.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 });
@@ -8021,6 +8047,7 @@ var BaseAddon = class {
8021
8047
  * resolved blob back is a no-op for every unchanged secret.
8022
8048
  */
8023
8049
  async resolveGlobalStore(nodeId, cap) {
8050
+ await this.loadScopedSchemas(cap);
8024
8051
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8025
8052
  return this.projectForNode(raw, nodeId, cap);
8026
8053
  }
@@ -8031,6 +8058,7 @@ var BaseAddon = class {
8031
8058
  }
8032
8059
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8033
8060
  async readRawProjectedStore(nodeId, cap) {
8061
+ await this.loadScopedSchemas(cap);
8034
8062
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8035
8063
  return this.projectForNode(raw, nodeId, cap);
8036
8064
  }
@@ -8133,6 +8161,7 @@ var BaseAddon = class {
8133
8161
  await this.applyGlobalPatch(patch, nodeId);
8134
8162
  }
8135
8163
  async applyGlobalPatch(barePatch, nodeId) {
8164
+ await this.loadScopedSchemas();
8136
8165
  const keys = this.perNodeKeys();
8137
8166
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8138
8167
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8146,7 +8175,8 @@ var BaseAddon = class {
8146
8175
  await this.resolveConfig();
8147
8176
  await this.onConfigChanged();
8148
8177
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8149
- 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)]);
8150
8180
  }
8151
8181
  /**
8152
8182
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8154,14 +8184,67 @@ var BaseAddon = class {
8154
8184
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8155
8185
  * settings API behaves exactly like the legacy node-agnostic one.
8156
8186
  */
8157
- _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
+ */
8158
8237
  perNodeKeys(cap) {
8159
8238
  const cacheKey = cap ?? "";
8160
- const cached = this._perNodeKeysCache.get(cacheKey);
8239
+ const cached = this._nodeKeysByCap.get(cacheKey);
8161
8240
  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);
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);
8165
8248
  return keys;
8166
8249
  }
8167
8250
  /**
@@ -8171,10 +8254,9 @@ var BaseAddon = class {
8171
8254
  * time to return its response before the addon is torn down and
8172
8255
  * re-initialised by `AddonRegistryService.restartAddon`.
8173
8256
  */
8174
- maybeAutoRestart(patchKeys, schema) {
8175
- if (!schema) return;
8257
+ maybeAutoRestart(patchKeys, schemas) {
8176
8258
  const restartKeys = /* @__PURE__ */ new Set();
8177
- 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) {
8178
8260
  if (field.type === "separator" || field.type === "info") continue;
8179
8261
  if (field.requiresRestart) restartKeys.add(field.key);
8180
8262
  }
@@ -8370,12 +8452,14 @@ var BaseAddon = class {
8370
8452
  * here, in the addon's own process, so the hub keeps no copy of it.
8371
8453
  */
8372
8454
  async getDeclaredSettingsKeys() {
8455
+ await this.loadScopedSchemas();
8373
8456
  return { keys: [...this.declaredKeySet()].toSorted() };
8374
8457
  }
8375
8458
  declaredKeySet() {
8376
8459
  const keys = new Set(Object.keys(this.defaults));
8377
8460
  const schema = this.globalSettingsSchema();
8378
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);
8379
8463
  for (const key of this._declaredStateKeys) keys.add(key);
8380
8464
  for (const path of this.secretStatePaths) {
8381
8465
  const root = path.split(/[.[]/, 1)[0];
@@ -8471,7 +8555,11 @@ var BaseAddon = class {
8471
8555
  }
8472
8556
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8473
8557
  addonSecretTree() {
8474
- 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
+ }
8475
8563
  return this._addonSecretTree;
8476
8564
  }
8477
8565
  deviceSecretTree() {
@@ -8721,6 +8809,23 @@ function dropPerNodeFields(fields) {
8721
8809
  }
8722
8810
  return kept;
8723
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
+ }
8724
8829
  function collectFieldKeys(fields) {
8725
8830
  const collected = [];
8726
8831
  for (const field of fields) {
@@ -13405,12 +13510,22 @@ var RecordIntegrationInstances = class {
13405
13510
  ...this.hooks.description,
13406
13511
  mintsId: true
13407
13512
  };
13408
- 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));
13409
13524
  listInstances = async () => {
13410
13525
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13411
13526
  };
13412
13527
  getInstance = async ({ id }) => {
13413
- const record = await this.handle.get(id);
13528
+ const record = await this.handle.get(await this.resolveRecordId(id));
13414
13529
  return record === null ? null : this.viewOf(record);
13415
13530
  };
13416
13531
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13427,7 +13542,8 @@ var RecordIntegrationInstances = class {
13427
13542
  await this.hooks.onChanged?.(minted, "created");
13428
13543
  return { id: minted };
13429
13544
  };
13430
- updateInstance = async ({ id, name, values }) => {
13545
+ updateInstance = async ({ id: askedId, name, values }) => {
13546
+ const id = await this.resolveRecordId(askedId);
13431
13547
  const patch = {};
13432
13548
  for (const [key, value] of Object.entries(values)) {
13433
13549
  if (value === void 0) continue;
@@ -13462,7 +13578,10 @@ var RecordIntegrationInstances = class {
13462
13578
  status: "unsupported",
13463
13579
  error: "This integration declares no connection test"
13464
13580
  };
13465
- return this.hooks.test(input);
13581
+ return this.hooks.test(input.id === null ? input : {
13582
+ ...input,
13583
+ id: await this.resolveRecordId(input.id)
13584
+ });
13466
13585
  };
13467
13586
  /** The flat provider object to hand to `registerProvider`. */
13468
13587
  toProvider() {
@@ -97556,7 +97675,7 @@ function connectionFromSettings(settings, tokenLocked) {
97556
97675
  function sameConnection(a, b) {
97557
97676
  return JSON.stringify(a) === JSON.stringify(b);
97558
97677
  }
97559
- /** 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. */
97560
97679
  function buildConnectionFormSchema() {
97561
97680
  return { sections: [
97562
97681
  {
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-9TIHFj1w.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 });
@@ -8020,6 +8046,7 @@ var BaseAddon = class {
8020
8046
  * resolved blob back is a no-op for every unchanged secret.
8021
8047
  */
8022
8048
  async resolveGlobalStore(nodeId, cap) {
8049
+ await this.loadScopedSchemas(cap);
8023
8050
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8024
8051
  return this.projectForNode(raw, nodeId, cap);
8025
8052
  }
@@ -8030,6 +8057,7 @@ var BaseAddon = class {
8030
8057
  }
8031
8058
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8032
8059
  async readRawProjectedStore(nodeId, cap) {
8060
+ await this.loadScopedSchemas(cap);
8033
8061
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8034
8062
  return this.projectForNode(raw, nodeId, cap);
8035
8063
  }
@@ -8132,6 +8160,7 @@ var BaseAddon = class {
8132
8160
  await this.applyGlobalPatch(patch, nodeId);
8133
8161
  }
8134
8162
  async applyGlobalPatch(barePatch, nodeId) {
8163
+ await this.loadScopedSchemas();
8135
8164
  const keys = this.perNodeKeys();
8136
8165
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8137
8166
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8145,7 +8174,8 @@ var BaseAddon = class {
8145
8174
  await this.resolveConfig();
8146
8175
  await this.onConfigChanged();
8147
8176
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8148
- 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)]);
8149
8179
  }
8150
8180
  /**
8151
8181
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8153,14 +8183,67 @@ var BaseAddon = class {
8153
8183
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8154
8184
  * settings API behaves exactly like the legacy node-agnostic one.
8155
8185
  */
8156
- _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
+ */
8157
8236
  perNodeKeys(cap) {
8158
8237
  const cacheKey = cap ?? "";
8159
- const cached = this._perNodeKeysCache.get(cacheKey);
8238
+ const cached = this._nodeKeysByCap.get(cacheKey);
8160
8239
  if (cached) return cached;
8161
- const schema = this.globalSettingsSchema(cap);
8162
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8163
- 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);
8164
8247
  return keys;
8165
8248
  }
8166
8249
  /**
@@ -8170,10 +8253,9 @@ var BaseAddon = class {
8170
8253
  * time to return its response before the addon is torn down and
8171
8254
  * re-initialised by `AddonRegistryService.restartAddon`.
8172
8255
  */
8173
- maybeAutoRestart(patchKeys, schema) {
8174
- if (!schema) return;
8256
+ maybeAutoRestart(patchKeys, schemas) {
8175
8257
  const restartKeys = /* @__PURE__ */ new Set();
8176
- 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) {
8177
8259
  if (field.type === "separator" || field.type === "info") continue;
8178
8260
  if (field.requiresRestart) restartKeys.add(field.key);
8179
8261
  }
@@ -8369,12 +8451,14 @@ var BaseAddon = class {
8369
8451
  * here, in the addon's own process, so the hub keeps no copy of it.
8370
8452
  */
8371
8453
  async getDeclaredSettingsKeys() {
8454
+ await this.loadScopedSchemas();
8372
8455
  return { keys: [...this.declaredKeySet()].toSorted() };
8373
8456
  }
8374
8457
  declaredKeySet() {
8375
8458
  const keys = new Set(Object.keys(this.defaults));
8376
8459
  const schema = this.globalSettingsSchema();
8377
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);
8378
8462
  for (const key of this._declaredStateKeys) keys.add(key);
8379
8463
  for (const path of this.secretStatePaths) {
8380
8464
  const root = path.split(/[.[]/, 1)[0];
@@ -8470,7 +8554,11 @@ var BaseAddon = class {
8470
8554
  }
8471
8555
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8472
8556
  addonSecretTree() {
8473
- 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
+ }
8474
8562
  return this._addonSecretTree;
8475
8563
  }
8476
8564
  deviceSecretTree() {
@@ -8720,6 +8808,23 @@ function dropPerNodeFields(fields) {
8720
8808
  }
8721
8809
  return kept;
8722
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
+ }
8723
8828
  function collectFieldKeys(fields) {
8724
8829
  const collected = [];
8725
8830
  for (const field of fields) {
@@ -13404,12 +13509,22 @@ var RecordIntegrationInstances = class {
13404
13509
  ...this.hooks.description,
13405
13510
  mintsId: true
13406
13511
  };
13407
- 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));
13408
13523
  listInstances = async () => {
13409
13524
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13410
13525
  };
13411
13526
  getInstance = async ({ id }) => {
13412
- const record = await this.handle.get(id);
13527
+ const record = await this.handle.get(await this.resolveRecordId(id));
13413
13528
  return record === null ? null : this.viewOf(record);
13414
13529
  };
13415
13530
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13426,7 +13541,8 @@ var RecordIntegrationInstances = class {
13426
13541
  await this.hooks.onChanged?.(minted, "created");
13427
13542
  return { id: minted };
13428
13543
  };
13429
- updateInstance = async ({ id, name, values }) => {
13544
+ updateInstance = async ({ id: askedId, name, values }) => {
13545
+ const id = await this.resolveRecordId(askedId);
13430
13546
  const patch = {};
13431
13547
  for (const [key, value] of Object.entries(values)) {
13432
13548
  if (value === void 0) continue;
@@ -13461,7 +13577,10 @@ var RecordIntegrationInstances = class {
13461
13577
  status: "unsupported",
13462
13578
  error: "This integration declares no connection test"
13463
13579
  };
13464
- return this.hooks.test(input);
13580
+ return this.hooks.test(input.id === null ? input : {
13581
+ ...input,
13582
+ id: await this.resolveRecordId(input.id)
13583
+ });
13465
13584
  };
13466
13585
  /** The flat provider object to hand to `registerProvider`. */
13467
13586
  toProvider() {
@@ -97555,7 +97674,7 @@ function connectionFromSettings(settings, tokenLocked) {
97555
97674
  function sameConnection(a, b) {
97556
97675
  return JSON.stringify(a) === JSON.stringify(b);
97557
97676
  }
97558
- /** 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. */
97559
97678
  function buildConnectionFormSchema() {
97560
97679
  return { sections: [
97561
97680
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-enpal",
3
- "version": "0.1.9",
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.770"
66
+ "server": ">=1.2.777"
67
67
  }
68
68
  },
69
69
  "files": [