@camstack/addon-smtp-nodemailer 1.2.147 → 1.2.148

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
+ ## [1.2.148] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - The relay form is declared as the integration instance form; the form and stored relays are unchanged.
15
+
10
16
  ## [1.2.147] - 2026-11-04
11
17
 
12
18
  ### Changed
@@ -6020,7 +6020,7 @@ var ZodIssueCode = {
6020
6020
  var ZodFirstPartyTypeKind;
6021
6021
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6022
6022
  //#endregion
6023
- //#region ../types/dist/sleep-BaWhXZFm.mjs
6023
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
6024
6024
  /**
6025
6025
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6026
6026
  * window to float samples (D455).
@@ -6620,7 +6620,7 @@ var NOT_RUN = {
6620
6620
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6621
6621
  * because the hub (and this addon's boot) must not hang on a migration.
6622
6622
  */
6623
- async function runAddonMigrations(host, migrations, logger) {
6623
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6624
6624
  const first = migrations[0];
6625
6625
  if (first === void 0) return NOT_RUN;
6626
6626
  const owner = first.meta.owner;
@@ -6649,7 +6649,9 @@ async function runAddonMigrations(host, migrations, logger) {
6649
6649
  else if (status === "blocked") blocked.push(id);
6650
6650
  else skipped.push(id);
6651
6651
  };
6652
+ const selected = options.only === void 0 ? null : new Set(options.only);
6652
6653
  for (const step of declaration.steps) {
6654
+ if (selected !== null && !selected.has(step.id)) continue;
6653
6655
  const bound = byId.get(step.id);
6654
6656
  if (bound === void 0 || step.action === "skip") {
6655
6657
  skipped.push(step.id);
@@ -6827,6 +6829,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6827
6829
  }
6828
6830
  return out;
6829
6831
  }
6832
+ /** Every scope, in the order the guard and the docs list them. */
6833
+ var SCOPED_SETTINGS_SCOPES = [
6834
+ "cluster",
6835
+ "device-defaults",
6836
+ "node"
6837
+ ];
6830
6838
  /**
6831
6839
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6832
6840
  *
@@ -7718,6 +7726,13 @@ var BaseAddon = class {
7718
7726
  */
7719
7727
  secretStatePaths = [];
7720
7728
  _addonSecretTree = null;
7729
+ /**
7730
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7731
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7732
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7733
+ */
7734
+ _scopedByCap = /* @__PURE__ */ new Map();
7735
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7721
7736
  _deviceSecretTree = null;
7722
7737
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7723
7738
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7787,6 +7802,7 @@ var BaseAddon = class {
7787
7802
  this._ctx = context;
7788
7803
  this._secretSettings = null;
7789
7804
  }
7805
+ await this.loadScopedSchemas();
7790
7806
  await this.resolveConfig();
7791
7807
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7792
7808
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7796,6 +7812,7 @@ var BaseAddon = class {
7796
7812
  const normalized = normalizeAddonInitResult(result);
7797
7813
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7798
7814
  this._registeredCapNames = providers.map((p) => p.capability.name);
7815
+ await this.loadScopedSchemas();
7799
7816
  return normalized;
7800
7817
  }
7801
7818
  /**
@@ -7809,14 +7826,18 @@ var BaseAddon = class {
7809
7826
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7810
7827
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7811
7828
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7829
+ *
7830
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7831
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7832
+ * omitted applied ids "newer").
7812
7833
  */
7813
- async runMigrations(migrations) {
7834
+ async runMigrations(migrations, options) {
7814
7835
  const api = this.ctx.api.migrations;
7815
7836
  const report = await runAddonMigrations({
7816
7837
  declareOwner: (input) => api.declareOwner.mutate(input),
7817
7838
  startMigration: (input) => api.startMigration.mutate(input),
7818
7839
  finishMigration: (input) => api.finishMigration.mutate(input)
7819
- }, migrations, this.ctx.logger);
7840
+ }, migrations, this.ctx.logger, options);
7820
7841
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7821
7842
  return report;
7822
7843
  }
@@ -7880,6 +7901,16 @@ var BaseAddon = class {
7880
7901
  schema(schema) {
7881
7902
  return schema;
7882
7903
  }
7904
+ /**
7905
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7906
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7907
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7908
+ * must already know which keys are per node. Names the providers register at init are added
7909
+ * once known.
7910
+ */
7911
+ scopedSettingsCapabilities() {
7912
+ return [];
7913
+ }
7883
7914
  /** Override to provide global-level settings UI schema. */
7884
7915
  globalSettingsSchema(_cap) {
7885
7916
  return null;
@@ -7923,7 +7954,8 @@ var BaseAddon = class {
7923
7954
  return [];
7924
7955
  }
7925
7956
  async getGlobalSettings(overlay, cap, nodeId) {
7926
- const schema = this.globalSettingsSchema(cap);
7957
+ await this.loadScopedSchemas(cap);
7958
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7927
7959
  if (!schema) return { sections: [] };
7928
7960
  const projected = await this.readRawProjectedStore(nodeId, cap);
7929
7961
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7958,10 +7990,9 @@ var BaseAddon = class {
7958
7990
  *
7959
7991
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7960
7992
  * needs its connection fields, the default integration the addon's own sections. It is the
7961
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7962
- * unchanged (`integrations.setSettings` for an account integration).
7993
+ * SHAPE of the form only — the values live on the instance record.
7963
7994
  */
7964
- async integrationSettingsSchema(_instance) {
7995
+ async integrationFormSchema(_instance) {
7965
7996
  return null;
7966
7997
  }
7967
7998
  /**
@@ -8007,7 +8038,7 @@ var BaseAddon = class {
8007
8038
  * per-instance schema when it declares one, else its global schema (what they always showed).
8008
8039
  */
8009
8040
  async getIntegrationFormSchema(instance, nodeId) {
8010
- const scoped = await this.integrationSettingsSchema(instance);
8041
+ const scoped = await this.integrationFormSchema(instance);
8011
8042
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8012
8043
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8013
8044
  }
@@ -8055,6 +8086,7 @@ var BaseAddon = class {
8055
8086
  * resolved blob back is a no-op for every unchanged secret.
8056
8087
  */
8057
8088
  async resolveGlobalStore(nodeId, cap) {
8089
+ await this.loadScopedSchemas(cap);
8058
8090
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8059
8091
  return this.projectForNode(raw, nodeId, cap);
8060
8092
  }
@@ -8065,6 +8097,7 @@ var BaseAddon = class {
8065
8097
  }
8066
8098
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8067
8099
  async readRawProjectedStore(nodeId, cap) {
8100
+ await this.loadScopedSchemas(cap);
8068
8101
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8069
8102
  return this.projectForNode(raw, nodeId, cap);
8070
8103
  }
@@ -8167,6 +8200,7 @@ var BaseAddon = class {
8167
8200
  await this.applyGlobalPatch(patch, nodeId);
8168
8201
  }
8169
8202
  async applyGlobalPatch(barePatch, nodeId) {
8203
+ await this.loadScopedSchemas();
8170
8204
  const keys = this.perNodeKeys();
8171
8205
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8172
8206
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8180,7 +8214,8 @@ var BaseAddon = class {
8180
8214
  await this.resolveConfig();
8181
8215
  await this.onConfigChanged();
8182
8216
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8183
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8217
+ const legacy = this.globalSettingsSchema();
8218
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8184
8219
  }
8185
8220
  /**
8186
8221
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8188,14 +8223,67 @@ var BaseAddon = class {
8188
8223
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8189
8224
  * settings API behaves exactly like the legacy node-agnostic one.
8190
8225
  */
8191
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8226
+ /**
8227
+ * Load the scoped schemas of every capability this addon declares (the override and the
8228
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8229
+ * derived from the previous set. Idempotent.
8230
+ */
8231
+ async loadScopedSchemas(extraCap) {
8232
+ const caps = new Set([
8233
+ ...this.scopedSettingsCapabilities(),
8234
+ ...this._registeredCapNames,
8235
+ ...extraCap === void 0 ? [] : [extraCap]
8236
+ ]);
8237
+ let added = false;
8238
+ for (const cap of caps) {
8239
+ if (this._scopedByCap.has(cap)) continue;
8240
+ const loaded = [];
8241
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8242
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8243
+ if (schema !== null) loaded.push({
8244
+ scope,
8245
+ schema
8246
+ });
8247
+ }
8248
+ this._scopedByCap.set(cap, loaded);
8249
+ added = true;
8250
+ }
8251
+ if (!added) return;
8252
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8253
+ this._addonSecretTree = null;
8254
+ }
8255
+ /**
8256
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8257
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8258
+ */
8259
+ scopedFormSchema(cap) {
8260
+ const byId = /* @__PURE__ */ new Map();
8261
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8262
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8263
+ }
8264
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8265
+ loadedScopedSchemas(cap) {
8266
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8267
+ return [...this._scopedByCap.values()].flat();
8268
+ }
8269
+ /**
8270
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8271
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8272
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8273
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8274
+ * Call {@link loadScopedSchemas} first (the async seams do).
8275
+ */
8192
8276
  perNodeKeys(cap) {
8193
8277
  const cacheKey = cap ?? "";
8194
- const cached = this._perNodeKeysCache.get(cacheKey);
8278
+ const cached = this._nodeKeysByCap.get(cacheKey);
8195
8279
  if (cached) return cached;
8196
- const schema = this.globalSettingsSchema(cap);
8197
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8198
- this._perNodeKeysCache.set(cacheKey, keys);
8280
+ const legacy = this.globalSettingsSchema(cap);
8281
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8282
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8283
+ if (scope !== "node") continue;
8284
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8285
+ }
8286
+ this._nodeKeysByCap.set(cacheKey, keys);
8199
8287
  return keys;
8200
8288
  }
8201
8289
  /**
@@ -8205,10 +8293,9 @@ var BaseAddon = class {
8205
8293
  * time to return its response before the addon is torn down and
8206
8294
  * re-initialised by `AddonRegistryService.restartAddon`.
8207
8295
  */
8208
- maybeAutoRestart(patchKeys, schema) {
8209
- if (!schema) return;
8296
+ maybeAutoRestart(patchKeys, schemas) {
8210
8297
  const restartKeys = /* @__PURE__ */ new Set();
8211
- for (const section of schema.sections) for (const field of section.fields) {
8298
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8212
8299
  if (field.type === "separator" || field.type === "info") continue;
8213
8300
  if (field.requiresRestart) restartKeys.add(field.key);
8214
8301
  }
@@ -8404,12 +8491,14 @@ var BaseAddon = class {
8404
8491
  * here, in the addon's own process, so the hub keeps no copy of it.
8405
8492
  */
8406
8493
  async getDeclaredSettingsKeys() {
8494
+ await this.loadScopedSchemas();
8407
8495
  return { keys: [...this.declaredKeySet()].toSorted() };
8408
8496
  }
8409
8497
  declaredKeySet() {
8410
8498
  const keys = new Set(Object.keys(this.defaults));
8411
8499
  const schema = this.globalSettingsSchema();
8412
8500
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8501
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8413
8502
  for (const key of this._declaredStateKeys) keys.add(key);
8414
8503
  for (const path of this.secretStatePaths) {
8415
8504
  const root = path.split(/[.[]/, 1)[0];
@@ -8505,7 +8594,11 @@ var BaseAddon = class {
8505
8594
  }
8506
8595
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8507
8596
  addonSecretTree() {
8508
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8597
+ if (this._addonSecretTree === null) {
8598
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8599
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8600
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8601
+ }
8509
8602
  return this._addonSecretTree;
8510
8603
  }
8511
8604
  deviceSecretTree() {
@@ -8755,6 +8848,23 @@ function dropPerNodeFields(fields) {
8755
8848
  }
8756
8849
  return kept;
8757
8850
  }
8851
+ /** The keys of the value-holding fields, recursing into layout containers. */
8852
+ function collectValueFieldKeys(fields) {
8853
+ const collected = [];
8854
+ for (const field of fields) {
8855
+ if (field.type === "group") {
8856
+ collected.push(...collectValueFieldKeys(field.fields));
8857
+ continue;
8858
+ }
8859
+ if (field.type === "sub-tabs") {
8860
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8861
+ continue;
8862
+ }
8863
+ if (field.type === "separator" || field.type === "info") continue;
8864
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8865
+ }
8866
+ return collected;
8867
+ }
8758
8868
  function collectFieldKeys(fields) {
8759
8869
  const collected = [];
8760
8870
  for (const field of fields) {
@@ -9288,6 +9398,7 @@ function defineEntryKind(name, spec) {
9288
9398
  class: "config",
9289
9399
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9290
9400
  retention: { none: "an entry goes only when the operator deletes it" },
9401
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9291
9402
  ...spec.database === void 0 ? {} : { database: spec.database },
9292
9403
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9293
9404
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11967,7 +12078,7 @@ function userError(code, params, englishFallback) {
11967
12078
  return new CamstackError(code, params, englishFallback);
11968
12079
  }
11969
12080
  //#endregion
11970
- //#region ../types/dist/composition-CRNwcFYY.mjs
12081
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11971
12082
  /**
11972
12083
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11973
12084
  *
@@ -13456,12 +13567,22 @@ var RecordIntegrationInstances = class {
13456
13567
  ...this.hooks.description,
13457
13568
  mintsId: true
13458
13569
  };
13459
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13570
+ /**
13571
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13572
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13573
+ * record id answers as it is. An id that names nothing comes back unchanged.
13574
+ */
13575
+ async resolveRecordId(id) {
13576
+ if (await this.handle.get(id) !== null) return id;
13577
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13578
+ return linked === void 0 ? id : linked.id;
13579
+ }
13580
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13460
13581
  listInstances = async () => {
13461
13582
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13462
13583
  };
13463
13584
  getInstance = async ({ id }) => {
13464
- const record = await this.handle.get(id);
13585
+ const record = await this.handle.get(await this.resolveRecordId(id));
13465
13586
  return record === null ? null : this.viewOf(record);
13466
13587
  };
13467
13588
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13478,7 +13599,8 @@ var RecordIntegrationInstances = class {
13478
13599
  await this.hooks.onChanged?.(minted, "created");
13479
13600
  return { id: minted };
13480
13601
  };
13481
- updateInstance = async ({ id, name, values }) => {
13602
+ updateInstance = async ({ id: askedId, name, values }) => {
13603
+ const id = await this.resolveRecordId(askedId);
13482
13604
  const patch = {};
13483
13605
  for (const [key, value] of Object.entries(values)) {
13484
13606
  if (value === void 0) continue;
@@ -13513,7 +13635,10 @@ var RecordIntegrationInstances = class {
13513
13635
  status: "unsupported",
13514
13636
  error: "This integration declares no connection test"
13515
13637
  };
13516
- return this.hooks.test(input);
13638
+ return this.hooks.test(input.id === null ? input : {
13639
+ ...input,
13640
+ id: await this.resolveRecordId(input.id)
13641
+ });
13517
13642
  };
13518
13643
  /** The flat provider object to hand to `registerProvider`. */
13519
13644
  toProvider() {
@@ -14575,7 +14700,13 @@ var MigrationLedgerEntrySchema = object({
14575
14700
  * joined by the engine from the catalogues it knows this boot. Absent when no
14576
14701
  * catalogue lists the id (a newer database).
14577
14702
  */
14578
- meta: MigrationMetaSchema.optional()
14703
+ meta: MigrationMetaSchema.optional(),
14704
+ /**
14705
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14706
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14707
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14708
+ */
14709
+ ownerNotLoaded: boolean().optional()
14579
14710
  });
14580
14711
  /** A catalogued migration that is not applied, with where it stands. */
14581
14712
  var MigrationPendingEntrySchema = object({
@@ -49115,6 +49246,9 @@ var RunningProposalEvidenceSchema = object({
49115
49246
  runsFound: number().int().nonnegative(),
49116
49247
  pausesFound: number().int().nonnegative(),
49117
49248
  longestPauseMs: number().nonnegative(),
49249
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
49250
+ pulsesFound: number().int().nonnegative(),
49251
+ longestPulseMs: number().nonnegative(),
49118
49252
  idleFloorW: number(),
49119
49253
  noiseCeilingW: number(),
49120
49254
  plateauW: number(),
@@ -54036,11 +54170,7 @@ var CreateIntegrationInputSchema = object({
54036
54170
  info: record(string(), unknown()).optional(),
54037
54171
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54038
54172
  originStableId: string().optional(),
54039
- settings: record(string(), unknown()).optional(),
54040
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
54041
- * the keychain, not the table (D787). */
54042
- secretKeys: array(string()).optional(),
54043
- skipRestart: boolean().optional()
54173
+ settings: record(string(), unknown()).optional()
54044
54174
  });
54045
54175
  var UpdateIntegrationInputSchema = object({
54046
54176
  id: string(),
@@ -54061,16 +54191,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54061
54191
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54062
54192
  kind: "mutation",
54063
54193
  auth: "admin"
54064
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
54065
- id: string(),
54066
- settings: record(string(), unknown()),
54067
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
54068
- secretKeys: array(string()).optional(),
54069
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
54070
- skipRestart: boolean().optional()
54071
- }), object({ success: boolean() }), {
54072
- kind: "mutation",
54073
- auth: "admin"
54074
54194
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54075
54195
  addonId: string(),
54076
54196
  settings: record(string(), unknown())
@@ -56795,6 +56915,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56795
56915
  }), {
56796
56916
  kind: "mutation",
56797
56917
  auth: "admin"
56918
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
56919
+ rows: number().int().nonnegative(),
56920
+ secrets: number().int().nonnegative()
56921
+ }), {
56922
+ kind: "mutation",
56923
+ auth: "admin"
56924
+ }), method(object({
56925
+ integrationId: string().min(1),
56926
+ patch: record(string(), unknown())
56927
+ }), object({ written: boolean() }), {
56928
+ kind: "mutation",
56929
+ auth: "admin"
56930
+ }), method(object({
56931
+ integrationId: string().min(1),
56932
+ keys: array(string().min(1))
56933
+ }), object({
56934
+ present: boolean(),
56935
+ removed: record(string(), unknown())
56936
+ }), {
56937
+ kind: "mutation",
56938
+ auth: "admin"
56798
56939
  });
56799
56940
  method(object({
56800
56941
  sourceUrl: string(),
@@ -75969,11 +76110,15 @@ discriminatedUnion("kind", [object({
75969
76110
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
75970
76111
  * near F. A device that is not near one level most of the time has no floor.
75971
76112
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
75972
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
75973
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
76113
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
76114
+ * display waking): it is neither a run nor a pause. Among the remaining
76115
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
76116
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
76117
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
76118
+ * is no run.
75974
76119
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
75975
- * grid a person would type; stop dwell covers the longest pause; start dwell
75976
- * covers the longest standby blip above T.
76120
+ * grid a person would type; stop dwell covers the longest pause and the
76121
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
75977
76122
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
75978
76123
  * run must hold exactly one true interval and nothing may flap outside.
75979
76124
  */
@@ -75984,6 +76129,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
75984
76129
  30 * SECOND_MS;
75985
76130
  60 * SECOND_MS;
75986
76131
  20 * MINUTE_MS$1;
76132
+ 90 * SECOND_MS;
76133
+ 30 * MINUTE_MS$1;
76134
+ 10 * MINUTE_MS$1;
76135
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
75987
76136
  10 * SECOND_MS;
75988
76137
  10 * SECOND_MS;
75989
76138
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -79739,13 +79888,6 @@ Object.freeze({
79739
79888
  access: "view",
79740
79889
  role: "admin"
79741
79890
  },
79742
- "integrations.getSettings": {
79743
- capName: "integrations",
79744
- capScope: "system",
79745
- addonId: null,
79746
- access: "view",
79747
- role: "admin"
79748
- },
79749
79891
  "integrations.list": {
79750
79892
  capName: "integrations",
79751
79893
  capScope: "system",
@@ -79753,13 +79895,6 @@ Object.freeze({
79753
79895
  access: "view",
79754
79896
  role: "admin"
79755
79897
  },
79756
- "integrations.setSettings": {
79757
- capName: "integrations",
79758
- capScope: "system",
79759
- addonId: null,
79760
- access: "create",
79761
- role: "admin"
79762
- },
79763
79898
  "integrations.testConnection": {
79764
79899
  capName: "integrations",
79765
79900
  capScope: "system",
@@ -80754,6 +80889,13 @@ Object.freeze({
80754
80889
  access: "view",
80755
80890
  role: "admin"
80756
80891
  },
80892
+ "migrations.clearLegacyIntegrationRows": {
80893
+ capName: "migrations",
80894
+ capScope: "system",
80895
+ addonId: null,
80896
+ access: "delete",
80897
+ role: "admin"
80898
+ },
80757
80899
  "migrations.declareOwner": {
80758
80900
  capName: "migrations",
80759
80901
  capScope: "system",
@@ -80761,6 +80903,13 @@ Object.freeze({
80761
80903
  access: "create",
80762
80904
  role: "admin"
80763
80905
  },
80906
+ "migrations.deleteLegacyIntegrationRowKeys": {
80907
+ capName: "migrations",
80908
+ capScope: "system",
80909
+ addonId: null,
80910
+ access: "delete",
80911
+ role: "admin"
80912
+ },
80764
80913
  "migrations.dryRun": {
80765
80914
  capName: "migrations",
80766
80915
  capScope: "system",
@@ -80796,6 +80945,20 @@ Object.freeze({
80796
80945
  access: "create",
80797
80946
  role: "admin"
80798
80947
  },
80948
+ "migrations.patchLegacyIntegrationRows": {
80949
+ capName: "migrations",
80950
+ capScope: "system",
80951
+ addonId: null,
80952
+ access: "create",
80953
+ role: "admin"
80954
+ },
80955
+ "migrations.readLegacyIntegrationRows": {
80956
+ capName: "migrations",
80957
+ capScope: "system",
80958
+ addonId: null,
80959
+ access: "view",
80960
+ role: "admin"
80961
+ },
80799
80962
  "migrations.restorePreMigration": {
80800
80963
  capName: "migrations",
80801
80964
  capScope: "system",
@@ -145846,7 +146009,8 @@ var SmtpNodemailerAddon = class extends BaseAddon {
145846
146009
  const rows = (await records.kind(smtpRelaysKind).query({ limit: RELAY_LIST_MAX })).records;
145847
146010
  return (rows.find((row) => row.data.host.length > 0) ?? rows[0])?.id;
145848
146011
  }
145849
- globalSettingsSchema() {
146012
+ /** The relay form. Its values live on the relay record, never in `addon-settings`. */
146013
+ async integrationFormSchema(_instance) {
145850
146014
  return this.relayForm();
145851
146015
  }
145852
146016
  async getGlobalSettings() {
@@ -6018,7 +6018,7 @@ var ZodIssueCode = {
6018
6018
  var ZodFirstPartyTypeKind;
6019
6019
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6020
6020
  //#endregion
6021
- //#region ../types/dist/sleep-BaWhXZFm.mjs
6021
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
6022
6022
  /**
6023
6023
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6024
6024
  * window to float samples (D455).
@@ -6618,7 +6618,7 @@ var NOT_RUN = {
6618
6618
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6619
6619
  * because the hub (and this addon's boot) must not hang on a migration.
6620
6620
  */
6621
- async function runAddonMigrations(host, migrations, logger) {
6621
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6622
6622
  const first = migrations[0];
6623
6623
  if (first === void 0) return NOT_RUN;
6624
6624
  const owner = first.meta.owner;
@@ -6647,7 +6647,9 @@ async function runAddonMigrations(host, migrations, logger) {
6647
6647
  else if (status === "blocked") blocked.push(id);
6648
6648
  else skipped.push(id);
6649
6649
  };
6650
+ const selected = options.only === void 0 ? null : new Set(options.only);
6650
6651
  for (const step of declaration.steps) {
6652
+ if (selected !== null && !selected.has(step.id)) continue;
6651
6653
  const bound = byId.get(step.id);
6652
6654
  if (bound === void 0 || step.action === "skip") {
6653
6655
  skipped.push(step.id);
@@ -6825,6 +6827,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6825
6827
  }
6826
6828
  return out;
6827
6829
  }
6830
+ /** Every scope, in the order the guard and the docs list them. */
6831
+ var SCOPED_SETTINGS_SCOPES = [
6832
+ "cluster",
6833
+ "device-defaults",
6834
+ "node"
6835
+ ];
6828
6836
  /**
6829
6837
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6830
6838
  *
@@ -7716,6 +7724,13 @@ var BaseAddon = class {
7716
7724
  */
7717
7725
  secretStatePaths = [];
7718
7726
  _addonSecretTree = null;
7727
+ /**
7728
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7729
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7730
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7731
+ */
7732
+ _scopedByCap = /* @__PURE__ */ new Map();
7733
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7719
7734
  _deviceSecretTree = null;
7720
7735
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7721
7736
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7785,6 +7800,7 @@ var BaseAddon = class {
7785
7800
  this._ctx = context;
7786
7801
  this._secretSettings = null;
7787
7802
  }
7803
+ await this.loadScopedSchemas();
7788
7804
  await this.resolveConfig();
7789
7805
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7790
7806
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7794,6 +7810,7 @@ var BaseAddon = class {
7794
7810
  const normalized = normalizeAddonInitResult(result);
7795
7811
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7796
7812
  this._registeredCapNames = providers.map((p) => p.capability.name);
7813
+ await this.loadScopedSchemas();
7797
7814
  return normalized;
7798
7815
  }
7799
7816
  /**
@@ -7807,14 +7824,18 @@ var BaseAddon = class {
7807
7824
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7808
7825
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7809
7826
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7827
+ *
7828
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7829
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7830
+ * omitted applied ids "newer").
7810
7831
  */
7811
- async runMigrations(migrations) {
7832
+ async runMigrations(migrations, options) {
7812
7833
  const api = this.ctx.api.migrations;
7813
7834
  const report = await runAddonMigrations({
7814
7835
  declareOwner: (input) => api.declareOwner.mutate(input),
7815
7836
  startMigration: (input) => api.startMigration.mutate(input),
7816
7837
  finishMigration: (input) => api.finishMigration.mutate(input)
7817
- }, migrations, this.ctx.logger);
7838
+ }, migrations, this.ctx.logger, options);
7818
7839
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7819
7840
  return report;
7820
7841
  }
@@ -7878,6 +7899,16 @@ var BaseAddon = class {
7878
7899
  schema(schema) {
7879
7900
  return schema;
7880
7901
  }
7902
+ /**
7903
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7904
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7905
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7906
+ * must already know which keys are per node. Names the providers register at init are added
7907
+ * once known.
7908
+ */
7909
+ scopedSettingsCapabilities() {
7910
+ return [];
7911
+ }
7881
7912
  /** Override to provide global-level settings UI schema. */
7882
7913
  globalSettingsSchema(_cap) {
7883
7914
  return null;
@@ -7921,7 +7952,8 @@ var BaseAddon = class {
7921
7952
  return [];
7922
7953
  }
7923
7954
  async getGlobalSettings(overlay, cap, nodeId) {
7924
- const schema = this.globalSettingsSchema(cap);
7955
+ await this.loadScopedSchemas(cap);
7956
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7925
7957
  if (!schema) return { sections: [] };
7926
7958
  const projected = await this.readRawProjectedStore(nodeId, cap);
7927
7959
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7956,10 +7988,9 @@ var BaseAddon = class {
7956
7988
  *
7957
7989
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7958
7990
  * needs its connection fields, the default integration the addon's own sections. It is the
7959
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7960
- * unchanged (`integrations.setSettings` for an account integration).
7991
+ * SHAPE of the form only — the values live on the instance record.
7961
7992
  */
7962
- async integrationSettingsSchema(_instance) {
7993
+ async integrationFormSchema(_instance) {
7963
7994
  return null;
7964
7995
  }
7965
7996
  /**
@@ -8005,7 +8036,7 @@ var BaseAddon = class {
8005
8036
  * per-instance schema when it declares one, else its global schema (what they always showed).
8006
8037
  */
8007
8038
  async getIntegrationFormSchema(instance, nodeId) {
8008
- const scoped = await this.integrationSettingsSchema(instance);
8039
+ const scoped = await this.integrationFormSchema(instance);
8009
8040
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8010
8041
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8011
8042
  }
@@ -8053,6 +8084,7 @@ var BaseAddon = class {
8053
8084
  * resolved blob back is a no-op for every unchanged secret.
8054
8085
  */
8055
8086
  async resolveGlobalStore(nodeId, cap) {
8087
+ await this.loadScopedSchemas(cap);
8056
8088
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8057
8089
  return this.projectForNode(raw, nodeId, cap);
8058
8090
  }
@@ -8063,6 +8095,7 @@ var BaseAddon = class {
8063
8095
  }
8064
8096
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8065
8097
  async readRawProjectedStore(nodeId, cap) {
8098
+ await this.loadScopedSchemas(cap);
8066
8099
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8067
8100
  return this.projectForNode(raw, nodeId, cap);
8068
8101
  }
@@ -8165,6 +8198,7 @@ var BaseAddon = class {
8165
8198
  await this.applyGlobalPatch(patch, nodeId);
8166
8199
  }
8167
8200
  async applyGlobalPatch(barePatch, nodeId) {
8201
+ await this.loadScopedSchemas();
8168
8202
  const keys = this.perNodeKeys();
8169
8203
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8170
8204
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8178,7 +8212,8 @@ var BaseAddon = class {
8178
8212
  await this.resolveConfig();
8179
8213
  await this.onConfigChanged();
8180
8214
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8181
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8215
+ const legacy = this.globalSettingsSchema();
8216
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8182
8217
  }
8183
8218
  /**
8184
8219
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8186,14 +8221,67 @@ var BaseAddon = class {
8186
8221
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8187
8222
  * settings API behaves exactly like the legacy node-agnostic one.
8188
8223
  */
8189
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8224
+ /**
8225
+ * Load the scoped schemas of every capability this addon declares (the override and the
8226
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8227
+ * derived from the previous set. Idempotent.
8228
+ */
8229
+ async loadScopedSchemas(extraCap) {
8230
+ const caps = new Set([
8231
+ ...this.scopedSettingsCapabilities(),
8232
+ ...this._registeredCapNames,
8233
+ ...extraCap === void 0 ? [] : [extraCap]
8234
+ ]);
8235
+ let added = false;
8236
+ for (const cap of caps) {
8237
+ if (this._scopedByCap.has(cap)) continue;
8238
+ const loaded = [];
8239
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8240
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8241
+ if (schema !== null) loaded.push({
8242
+ scope,
8243
+ schema
8244
+ });
8245
+ }
8246
+ this._scopedByCap.set(cap, loaded);
8247
+ added = true;
8248
+ }
8249
+ if (!added) return;
8250
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8251
+ this._addonSecretTree = null;
8252
+ }
8253
+ /**
8254
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8255
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8256
+ */
8257
+ scopedFormSchema(cap) {
8258
+ const byId = /* @__PURE__ */ new Map();
8259
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8260
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8261
+ }
8262
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8263
+ loadedScopedSchemas(cap) {
8264
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8265
+ return [...this._scopedByCap.values()].flat();
8266
+ }
8267
+ /**
8268
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8269
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8270
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8271
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8272
+ * Call {@link loadScopedSchemas} first (the async seams do).
8273
+ */
8190
8274
  perNodeKeys(cap) {
8191
8275
  const cacheKey = cap ?? "";
8192
- const cached = this._perNodeKeysCache.get(cacheKey);
8276
+ const cached = this._nodeKeysByCap.get(cacheKey);
8193
8277
  if (cached) return cached;
8194
- const schema = this.globalSettingsSchema(cap);
8195
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8196
- this._perNodeKeysCache.set(cacheKey, keys);
8278
+ const legacy = this.globalSettingsSchema(cap);
8279
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8280
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8281
+ if (scope !== "node") continue;
8282
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8283
+ }
8284
+ this._nodeKeysByCap.set(cacheKey, keys);
8197
8285
  return keys;
8198
8286
  }
8199
8287
  /**
@@ -8203,10 +8291,9 @@ var BaseAddon = class {
8203
8291
  * time to return its response before the addon is torn down and
8204
8292
  * re-initialised by `AddonRegistryService.restartAddon`.
8205
8293
  */
8206
- maybeAutoRestart(patchKeys, schema) {
8207
- if (!schema) return;
8294
+ maybeAutoRestart(patchKeys, schemas) {
8208
8295
  const restartKeys = /* @__PURE__ */ new Set();
8209
- for (const section of schema.sections) for (const field of section.fields) {
8296
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8210
8297
  if (field.type === "separator" || field.type === "info") continue;
8211
8298
  if (field.requiresRestart) restartKeys.add(field.key);
8212
8299
  }
@@ -8402,12 +8489,14 @@ var BaseAddon = class {
8402
8489
  * here, in the addon's own process, so the hub keeps no copy of it.
8403
8490
  */
8404
8491
  async getDeclaredSettingsKeys() {
8492
+ await this.loadScopedSchemas();
8405
8493
  return { keys: [...this.declaredKeySet()].toSorted() };
8406
8494
  }
8407
8495
  declaredKeySet() {
8408
8496
  const keys = new Set(Object.keys(this.defaults));
8409
8497
  const schema = this.globalSettingsSchema();
8410
8498
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8499
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8411
8500
  for (const key of this._declaredStateKeys) keys.add(key);
8412
8501
  for (const path of this.secretStatePaths) {
8413
8502
  const root = path.split(/[.[]/, 1)[0];
@@ -8503,7 +8592,11 @@ var BaseAddon = class {
8503
8592
  }
8504
8593
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8505
8594
  addonSecretTree() {
8506
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8595
+ if (this._addonSecretTree === null) {
8596
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8597
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8598
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8599
+ }
8507
8600
  return this._addonSecretTree;
8508
8601
  }
8509
8602
  deviceSecretTree() {
@@ -8753,6 +8846,23 @@ function dropPerNodeFields(fields) {
8753
8846
  }
8754
8847
  return kept;
8755
8848
  }
8849
+ /** The keys of the value-holding fields, recursing into layout containers. */
8850
+ function collectValueFieldKeys(fields) {
8851
+ const collected = [];
8852
+ for (const field of fields) {
8853
+ if (field.type === "group") {
8854
+ collected.push(...collectValueFieldKeys(field.fields));
8855
+ continue;
8856
+ }
8857
+ if (field.type === "sub-tabs") {
8858
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8859
+ continue;
8860
+ }
8861
+ if (field.type === "separator" || field.type === "info") continue;
8862
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8863
+ }
8864
+ return collected;
8865
+ }
8756
8866
  function collectFieldKeys(fields) {
8757
8867
  const collected = [];
8758
8868
  for (const field of fields) {
@@ -9286,6 +9396,7 @@ function defineEntryKind(name, spec) {
9286
9396
  class: "config",
9287
9397
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9288
9398
  retention: { none: "an entry goes only when the operator deletes it" },
9399
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9289
9400
  ...spec.database === void 0 ? {} : { database: spec.database },
9290
9401
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9291
9402
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11965,7 +12076,7 @@ function userError(code, params, englishFallback) {
11965
12076
  return new CamstackError(code, params, englishFallback);
11966
12077
  }
11967
12078
  //#endregion
11968
- //#region ../types/dist/composition-CRNwcFYY.mjs
12079
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11969
12080
  /**
11970
12081
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11971
12082
  *
@@ -13454,12 +13565,22 @@ var RecordIntegrationInstances = class {
13454
13565
  ...this.hooks.description,
13455
13566
  mintsId: true
13456
13567
  };
13457
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13568
+ /**
13569
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13570
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13571
+ * record id answers as it is. An id that names nothing comes back unchanged.
13572
+ */
13573
+ async resolveRecordId(id) {
13574
+ if (await this.handle.get(id) !== null) return id;
13575
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13576
+ return linked === void 0 ? id : linked.id;
13577
+ }
13578
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13458
13579
  listInstances = async () => {
13459
13580
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13460
13581
  };
13461
13582
  getInstance = async ({ id }) => {
13462
- const record = await this.handle.get(id);
13583
+ const record = await this.handle.get(await this.resolveRecordId(id));
13463
13584
  return record === null ? null : this.viewOf(record);
13464
13585
  };
13465
13586
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13476,7 +13597,8 @@ var RecordIntegrationInstances = class {
13476
13597
  await this.hooks.onChanged?.(minted, "created");
13477
13598
  return { id: minted };
13478
13599
  };
13479
- updateInstance = async ({ id, name, values }) => {
13600
+ updateInstance = async ({ id: askedId, name, values }) => {
13601
+ const id = await this.resolveRecordId(askedId);
13480
13602
  const patch = {};
13481
13603
  for (const [key, value] of Object.entries(values)) {
13482
13604
  if (value === void 0) continue;
@@ -13511,7 +13633,10 @@ var RecordIntegrationInstances = class {
13511
13633
  status: "unsupported",
13512
13634
  error: "This integration declares no connection test"
13513
13635
  };
13514
- return this.hooks.test(input);
13636
+ return this.hooks.test(input.id === null ? input : {
13637
+ ...input,
13638
+ id: await this.resolveRecordId(input.id)
13639
+ });
13515
13640
  };
13516
13641
  /** The flat provider object to hand to `registerProvider`. */
13517
13642
  toProvider() {
@@ -14573,7 +14698,13 @@ var MigrationLedgerEntrySchema = object({
14573
14698
  * joined by the engine from the catalogues it knows this boot. Absent when no
14574
14699
  * catalogue lists the id (a newer database).
14575
14700
  */
14576
- meta: MigrationMetaSchema.optional()
14701
+ meta: MigrationMetaSchema.optional(),
14702
+ /**
14703
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14704
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14705
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14706
+ */
14707
+ ownerNotLoaded: boolean().optional()
14577
14708
  });
14578
14709
  /** A catalogued migration that is not applied, with where it stands. */
14579
14710
  var MigrationPendingEntrySchema = object({
@@ -49113,6 +49244,9 @@ var RunningProposalEvidenceSchema = object({
49113
49244
  runsFound: number().int().nonnegative(),
49114
49245
  pausesFound: number().int().nonnegative(),
49115
49246
  longestPauseMs: number().nonnegative(),
49247
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
49248
+ pulsesFound: number().int().nonnegative(),
49249
+ longestPulseMs: number().nonnegative(),
49116
49250
  idleFloorW: number(),
49117
49251
  noiseCeilingW: number(),
49118
49252
  plateauW: number(),
@@ -54034,11 +54168,7 @@ var CreateIntegrationInputSchema = object({
54034
54168
  info: record(string(), unknown()).optional(),
54035
54169
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54036
54170
  originStableId: string().optional(),
54037
- settings: record(string(), unknown()).optional(),
54038
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
54039
- * the keychain, not the table (D787). */
54040
- secretKeys: array(string()).optional(),
54041
- skipRestart: boolean().optional()
54171
+ settings: record(string(), unknown()).optional()
54042
54172
  });
54043
54173
  var UpdateIntegrationInputSchema = object({
54044
54174
  id: string(),
@@ -54059,16 +54189,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54059
54189
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54060
54190
  kind: "mutation",
54061
54191
  auth: "admin"
54062
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
54063
- id: string(),
54064
- settings: record(string(), unknown()),
54065
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
54066
- secretKeys: array(string()).optional(),
54067
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
54068
- skipRestart: boolean().optional()
54069
- }), object({ success: boolean() }), {
54070
- kind: "mutation",
54071
- auth: "admin"
54072
54192
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54073
54193
  addonId: string(),
54074
54194
  settings: record(string(), unknown())
@@ -56793,6 +56913,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56793
56913
  }), {
56794
56914
  kind: "mutation",
56795
56915
  auth: "admin"
56916
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
56917
+ rows: number().int().nonnegative(),
56918
+ secrets: number().int().nonnegative()
56919
+ }), {
56920
+ kind: "mutation",
56921
+ auth: "admin"
56922
+ }), method(object({
56923
+ integrationId: string().min(1),
56924
+ patch: record(string(), unknown())
56925
+ }), object({ written: boolean() }), {
56926
+ kind: "mutation",
56927
+ auth: "admin"
56928
+ }), method(object({
56929
+ integrationId: string().min(1),
56930
+ keys: array(string().min(1))
56931
+ }), object({
56932
+ present: boolean(),
56933
+ removed: record(string(), unknown())
56934
+ }), {
56935
+ kind: "mutation",
56936
+ auth: "admin"
56796
56937
  });
56797
56938
  method(object({
56798
56939
  sourceUrl: string(),
@@ -75967,11 +76108,15 @@ discriminatedUnion("kind", [object({
75967
76108
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
75968
76109
  * near F. A device that is not near one level most of the time has no floor.
75969
76110
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
75970
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
75971
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
76111
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
76112
+ * display waking): it is neither a run nor a pause. Among the remaining
76113
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
76114
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
76115
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
76116
+ * is no run.
75972
76117
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
75973
- * grid a person would type; stop dwell covers the longest pause; start dwell
75974
- * covers the longest standby blip above T.
76118
+ * grid a person would type; stop dwell covers the longest pause and the
76119
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
75975
76120
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
75976
76121
  * run must hold exactly one true interval and nothing may flap outside.
75977
76122
  */
@@ -75982,6 +76127,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
75982
76127
  30 * SECOND_MS;
75983
76128
  60 * SECOND_MS;
75984
76129
  20 * MINUTE_MS$1;
76130
+ 90 * SECOND_MS;
76131
+ 30 * MINUTE_MS$1;
76132
+ 10 * MINUTE_MS$1;
76133
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
75985
76134
  10 * SECOND_MS;
75986
76135
  10 * SECOND_MS;
75987
76136
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -79737,13 +79886,6 @@ Object.freeze({
79737
79886
  access: "view",
79738
79887
  role: "admin"
79739
79888
  },
79740
- "integrations.getSettings": {
79741
- capName: "integrations",
79742
- capScope: "system",
79743
- addonId: null,
79744
- access: "view",
79745
- role: "admin"
79746
- },
79747
79889
  "integrations.list": {
79748
79890
  capName: "integrations",
79749
79891
  capScope: "system",
@@ -79751,13 +79893,6 @@ Object.freeze({
79751
79893
  access: "view",
79752
79894
  role: "admin"
79753
79895
  },
79754
- "integrations.setSettings": {
79755
- capName: "integrations",
79756
- capScope: "system",
79757
- addonId: null,
79758
- access: "create",
79759
- role: "admin"
79760
- },
79761
79896
  "integrations.testConnection": {
79762
79897
  capName: "integrations",
79763
79898
  capScope: "system",
@@ -80752,6 +80887,13 @@ Object.freeze({
80752
80887
  access: "view",
80753
80888
  role: "admin"
80754
80889
  },
80890
+ "migrations.clearLegacyIntegrationRows": {
80891
+ capName: "migrations",
80892
+ capScope: "system",
80893
+ addonId: null,
80894
+ access: "delete",
80895
+ role: "admin"
80896
+ },
80755
80897
  "migrations.declareOwner": {
80756
80898
  capName: "migrations",
80757
80899
  capScope: "system",
@@ -80759,6 +80901,13 @@ Object.freeze({
80759
80901
  access: "create",
80760
80902
  role: "admin"
80761
80903
  },
80904
+ "migrations.deleteLegacyIntegrationRowKeys": {
80905
+ capName: "migrations",
80906
+ capScope: "system",
80907
+ addonId: null,
80908
+ access: "delete",
80909
+ role: "admin"
80910
+ },
80762
80911
  "migrations.dryRun": {
80763
80912
  capName: "migrations",
80764
80913
  capScope: "system",
@@ -80794,6 +80943,20 @@ Object.freeze({
80794
80943
  access: "create",
80795
80944
  role: "admin"
80796
80945
  },
80946
+ "migrations.patchLegacyIntegrationRows": {
80947
+ capName: "migrations",
80948
+ capScope: "system",
80949
+ addonId: null,
80950
+ access: "create",
80951
+ role: "admin"
80952
+ },
80953
+ "migrations.readLegacyIntegrationRows": {
80954
+ capName: "migrations",
80955
+ capScope: "system",
80956
+ addonId: null,
80957
+ access: "view",
80958
+ role: "admin"
80959
+ },
80797
80960
  "migrations.restorePreMigration": {
80798
80961
  capName: "migrations",
80799
80962
  capScope: "system",
@@ -145844,7 +146007,8 @@ var SmtpNodemailerAddon = class extends BaseAddon {
145844
146007
  const rows = (await records.kind(smtpRelaysKind).query({ limit: RELAY_LIST_MAX })).records;
145845
146008
  return (rows.find((row) => row.data.host.length > 0) ?? rows[0])?.id;
145846
146009
  }
145847
- globalSettingsSchema() {
146010
+ /** The relay form. Its values live on the relay record, never in `addon-settings`. */
146011
+ async integrationFormSchema(_instance) {
145848
146012
  return this.relayForm();
145849
146013
  }
145850
146014
  async getGlobalSettings() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-smtp-nodemailer",
3
- "version": "1.2.147",
3
+ "version": "1.2.148",
4
4
  "description": "SMTP email provider addon for CamStack — wraps `nodemailer` and registers a `smtp-provider` cap collection entry. Used by notifier addons.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -59,7 +59,7 @@
59
59
  "smtp-server"
60
60
  ],
61
61
  "requires": {
62
- "server": ">=1.2.762"
62
+ "server": ">=1.2.777"
63
63
  }
64
64
  },
65
65
  "files": [