@camstack/addon-smtp-nodemailer 1.2.150 → 1.2.152

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.
@@ -38,7 +38,7 @@ var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["modu
38
38
  //#endregion
39
39
  let node_crypto = require("node:crypto");
40
40
  let node_net = require("node:net");
41
- //#region ../types/dist/event-category-BmPm98n0.mjs
41
+ //#region ../types/dist/event-category-78tcBemo.mjs
42
42
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
43
43
  EventCategory["SystemBoot"] = "system.boot";
44
44
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -159,6 +159,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
159
159
  */
160
160
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
161
161
  /**
162
+ * An addon is RUNNING although its installed package is below what the
163
+ * running server requires (`camstack.requires.addons` of the server, or the
164
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
165
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
166
+ * operator alert per node and package. The addon is never held back for it.
167
+ *
168
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
169
+ */
170
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
171
+ /**
172
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
173
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
174
+ *
175
+ * Payload: `{ packageName, addonId, nodeId }`.
176
+ */
177
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
178
+ /**
162
179
  * Monitor scheduled the next retry for a failed addon. Transient —
163
180
  * surfaced to the UI for live-updating the "next retry in Ns"
164
181
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -269,7 +286,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
269
286
  EventCategory["ProcessRestarted"] = "process.restarted";
270
287
  /**
271
288
  * The SET of storage locations changed — one was created, edited, enabled,
272
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
289
+ * disabled or deleted through the location instances or the forced-delete action.
273
290
  *
274
291
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
275
292
  * it must also converge on its own periodic path, because a dropped event
@@ -6020,7 +6037,7 @@ var ZodIssueCode = {
6020
6037
  var ZodFirstPartyTypeKind;
6021
6038
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6022
6039
  //#endregion
6023
- //#region ../types/dist/sleep-CmjWurK5.mjs
6040
+ //#region ../types/dist/sleep-CQP-REJp.mjs
6024
6041
  /**
6025
6042
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6026
6043
  * window to float samples (D455).
@@ -6384,6 +6401,59 @@ Object.fromEntries([
6384
6401
  function isValuelessField(field) {
6385
6402
  return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button" || field.type === "account-sign-in" || field.type === "secret-presence";
6386
6403
  }
6404
+ /**
6405
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6406
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6407
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6408
+ * pre-resolving) lets callers both read the effective value AND detect
6409
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6410
+ * malformed input yields an empty map, never throws.
6411
+ */
6412
+ function collectHydratedFieldEntries(schema) {
6413
+ const out = /* @__PURE__ */ new Map();
6414
+ if (typeof schema !== "object" || schema === null) return out;
6415
+ const sections = schema.sections;
6416
+ if (!Array.isArray(sections)) return out;
6417
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6418
+ return out;
6419
+ }
6420
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6421
+ function collectFieldEntriesInto(fields, into) {
6422
+ if (!Array.isArray(fields)) return;
6423
+ for (const field of fields) {
6424
+ if (typeof field !== "object" || field === null) continue;
6425
+ const type = field.type;
6426
+ if (type === "group") {
6427
+ collectFieldEntriesInto(field.fields, into);
6428
+ continue;
6429
+ }
6430
+ if (type === "sub-tabs") {
6431
+ const tabs = field.tabs;
6432
+ if (Array.isArray(tabs)) {
6433
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6434
+ }
6435
+ continue;
6436
+ }
6437
+ const key = field.key;
6438
+ if (typeof key !== "string" || key.length === 0) continue;
6439
+ if (!("value" in field)) continue;
6440
+ into.set(key, {
6441
+ value: field.value,
6442
+ default: field.default
6443
+ });
6444
+ }
6445
+ }
6446
+ /**
6447
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6448
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6449
+ * common "just give me the resolved values" case. Built on
6450
+ * {@link collectHydratedFieldEntries}.
6451
+ */
6452
+ function collectHydratedFieldValues(schema) {
6453
+ const out = /* @__PURE__ */ new Map();
6454
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6455
+ return out;
6456
+ }
6387
6457
  function hydrateSchema(schema, values, options = {}) {
6388
6458
  return {
6389
6459
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -6729,10 +6799,33 @@ function integrationRowsApiOver(api) {
6729
6799
  };
6730
6800
  }
6731
6801
  var PREFIX = "records/";
6802
+ /**
6803
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6804
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6805
+ * instance of the host's classes, not of the addon's. The brand plus the code
6806
+ * make `instanceof` answer by identity of the failure, not of the copy.
6807
+ */
6808
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6809
+ function carriesBrand(value) {
6810
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6811
+ }
6732
6812
  /** The base of every records failure. `detail` is the human part of the message. */
6733
6813
  var RecordsError = class extends Error {
6814
+ /** The code a subclass stands for; the base stands for every code. */
6815
+ static code = void 0;
6816
+ /**
6817
+ * `instanceof` across copies of this module: any branded error is a
6818
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6819
+ * subclass's code. Same-copy answers are unchanged.
6820
+ */
6821
+ static [Symbol.hasInstance](value) {
6822
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6823
+ if (!carriesBrand(value)) return false;
6824
+ return this.code === void 0 || value.code === this.code;
6825
+ }
6734
6826
  code;
6735
6827
  detail;
6828
+ [RECORDS_ERROR_BRAND] = true;
6736
6829
  constructor(code, detail) {
6737
6830
  super(`${PREFIX}${code}: ${detail}`);
6738
6831
  this.name = "RecordsError";
@@ -6741,29 +6834,33 @@ var RecordsError = class extends Error {
6741
6834
  }
6742
6835
  };
6743
6836
  /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
6744
- var RevConflictError = class extends RecordsError {
6837
+ var RevConflictError = class RevConflictError extends RecordsError {
6838
+ static code = "rev-conflict";
6745
6839
  constructor(detail) {
6746
- super("rev-conflict", detail);
6840
+ super(RevConflictError.code, detail);
6747
6841
  this.name = "RevConflictError";
6748
6842
  }
6749
6843
  };
6750
6844
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6751
- var RecordSecretUnavailableError = class extends RecordsError {
6845
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6846
+ static code = "secret-unavailable";
6752
6847
  constructor(detail) {
6753
- super("secret-unavailable", detail);
6848
+ super(RecordSecretUnavailableError.code, detail);
6754
6849
  this.name = "RecordSecretUnavailableError";
6755
6850
  }
6756
6851
  };
6757
- var InvalidRecordError = class extends RecordsError {
6852
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6853
+ static code = "invalid-record";
6758
6854
  constructor(detail) {
6759
- super("invalid-record", detail);
6855
+ super(InvalidRecordError.code, detail);
6760
6856
  this.name = "InvalidRecordError";
6761
6857
  }
6762
6858
  };
6763
6859
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6764
- var DeclarationRefusedError = class extends RecordsError {
6860
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6861
+ static code = "declaration-refused";
6765
6862
  constructor(detail) {
6766
- super("declaration-refused", detail);
6863
+ super(DeclarationRefusedError.code, detail);
6767
6864
  this.name = "DeclarationRefusedError";
6768
6865
  }
6769
6866
  };
@@ -7241,17 +7338,35 @@ var RecordIntegrationInstances = class {
7241
7338
  ...kind !== void 0 ? { kind } : {}
7242
7339
  });
7243
7340
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7341
+ const initial = this.hooks.initialBody(name, kind);
7342
+ const patch = this.patchOf(values ?? {});
7343
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7344
+ kind,
7345
+ body: {
7346
+ ...initial,
7347
+ ...patch
7348
+ }
7349
+ }));
7244
7350
  await this.handle.put({
7245
7351
  id: minted,
7246
- data: this.hooks.initialBody(name, kind)
7352
+ data: initial
7247
7353
  }, { ifRev: 0 });
7248
- const patch = this.patchOf(values ?? {});
7249
7354
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7250
7355
  await this.hooks.onChanged?.(minted, "created");
7251
7356
  return { id: minted };
7252
7357
  };
7253
7358
  updateInstance = async ({ id, name, values }) => {
7254
7359
  const patch = this.patchOf(values);
7360
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7361
+ const stored = await this.handle.get(id);
7362
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7363
+ kind: void 0,
7364
+ body: {
7365
+ ...stored.data,
7366
+ ...patch
7367
+ }
7368
+ }));
7369
+ }
7255
7370
  if (name !== void 0) patch[this.nameField] = name;
7256
7371
  await this.handle.patch(id, patch);
7257
7372
  await this.hooks.onChanged?.(id, "updated");
@@ -7562,6 +7677,21 @@ var SCOPED_SETTINGS_SCOPES = [
7562
7677
  "device-defaults",
7563
7678
  "node"
7564
7679
  ];
7680
+ /** The one refusal of a key a scope did not declare. */
7681
+ function assertDeclaredKeys(keys, declared) {
7682
+ const undeclared = keys.filter((key) => !declared.has(key));
7683
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7684
+ }
7685
+ /** Build the handle over an addon's scoped read, write and unset. */
7686
+ function createScopedSettings(deps) {
7687
+ return {
7688
+ async get() {
7689
+ return deps.schema.parse(await deps.read());
7690
+ },
7691
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7692
+ unset: (keys) => deps.unset(keys)
7693
+ };
7694
+ }
7565
7695
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7566
7696
  var KeychainWriteRefusedError = class extends Error {
7567
7697
  fieldPath;
@@ -8137,7 +8267,16 @@ function createHandle(raw, opts) {
8137
8267
  return blob;
8138
8268
  }
8139
8269
  },
8140
- writeAddonStore: writeAddon
8270
+ writeAddonStore: writeAddon,
8271
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8272
+ const stored = await readStoredAddonBlob(raw);
8273
+ index.seen("addon", stored);
8274
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8275
+ if (removable.length === 0) return;
8276
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8277
+ await raw.writeAddonStore(patch);
8278
+ index.wrote("addon", patch);
8279
+ })
8141
8280
  };
8142
8281
  }
8143
8282
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8682,6 +8821,151 @@ var BaseAddon = class {
8682
8821
  }
8683
8822
  }
8684
8823
  /**
8824
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8825
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8826
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8827
+ */
8828
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8829
+ return null;
8830
+ }
8831
+ /**
8832
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8833
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8834
+ */
8835
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8836
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8837
+ clusterSettings(cap, schema) {
8838
+ return this.scopedHandle("cluster", cap, schema);
8839
+ }
8840
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8841
+ deviceDefaults(cap, schema) {
8842
+ return this.scopedHandle("device-defaults", cap, schema);
8843
+ }
8844
+ /**
8845
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8846
+ * with no row yields the schema defaults — never a bare-key fallback.
8847
+ */
8848
+ nodeSettings(cap, schema, nodeId) {
8849
+ return this.scopedHandle("node", cap, schema, nodeId);
8850
+ }
8851
+ /**
8852
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8853
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8854
+ * central delete. `nodeId` omitted ⇒ the local node.
8855
+ */
8856
+ nodeState(key, schema, fallback, nodeId) {
8857
+ this._declaredStateKeys.add(key);
8858
+ return createDurableState({
8859
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8860
+ schema,
8861
+ fallback,
8862
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8863
+ write: async (patch) => {
8864
+ await this._ctx?.settings?.writeAddonStore(patch);
8865
+ },
8866
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8867
+ addonId: this._ctx?.id,
8868
+ key: k,
8869
+ error: String(e)
8870
+ } })
8871
+ });
8872
+ }
8873
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8874
+ async declaredScopedSchema(scope, cap) {
8875
+ await this.loadScopedSchemas(cap);
8876
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8877
+ }
8878
+ async requireScopedSchema(scope, cap) {
8879
+ const schema = await this.declaredScopedSchema(scope, cap);
8880
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8881
+ return schema;
8882
+ }
8883
+ scopedKeys(schema) {
8884
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8885
+ }
8886
+ /** `store` narrowed to the keys `schema` declares. */
8887
+ narrowToScope(store, schema) {
8888
+ const keys = this.scopedKeys(schema);
8889
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8890
+ }
8891
+ scopedHandle(scope, cap, schema, nodeId) {
8892
+ return createScopedSettings({
8893
+ schema,
8894
+ read: async () => {
8895
+ const declared = await this.requireScopedSchema(scope, cap);
8896
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8897
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8898
+ return {
8899
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8900
+ ...stored
8901
+ };
8902
+ },
8903
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8904
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8905
+ });
8906
+ }
8907
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8908
+ async writeScoped(scope, cap, patch, nodeId) {
8909
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8910
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8911
+ }
8912
+ /**
8913
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8914
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8915
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8916
+ */
8917
+ async unsetScoped(scope, cap, keys, nodeId) {
8918
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8919
+ const target = scope === "node" ? nodeId : void 0;
8920
+ const tree = this.addonSecretTree();
8921
+ const secretKeys = keys.filter((key) => tree.has(key));
8922
+ const plainKeys = keys.filter((key) => !tree.has(key));
8923
+ let changed = false;
8924
+ if (plainKeys.length > 0) {
8925
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8926
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8927
+ }
8928
+ if (secretKeys.length > 0) {
8929
+ await this.clearSecretKeys(secretKeys, target, cap);
8930
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8931
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8932
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8933
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8934
+ changed = true;
8935
+ }
8936
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8937
+ }
8938
+ /**
8939
+ * The hydrated form of one scope of one capability's settings (any
8940
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8941
+ */
8942
+ async getScopedSettings(scope, cap, nodeId) {
8943
+ const schema = await this.declaredScopedSchema(scope, cap);
8944
+ if (schema === null) return null;
8945
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8946
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8947
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8948
+ }
8949
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8950
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8951
+ this._operatorWriteDepth++;
8952
+ try {
8953
+ await this.writeScoped(scope, cap, patch, nodeId);
8954
+ } finally {
8955
+ this._operatorWriteDepth--;
8956
+ }
8957
+ }
8958
+ /**
8959
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8960
+ * its value. `null` when the capability declares nothing for the scope.
8961
+ */
8962
+ async readScopedValues(scope, cap, nodeId) {
8963
+ const schema = await this.declaredScopedSchema(scope, cap);
8964
+ if (schema === null) return null;
8965
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8966
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8967
+ }
8968
+ /**
8685
8969
  * What the create wizard and the integration edit page render for one instance: the addon's
8686
8970
  * per-instance schema when it declares one, else its global schema (what they always showed).
8687
8971
  */
@@ -8833,6 +9117,9 @@ var BaseAddon = class {
8833
9117
  * a keychain (an older kernel) it writes `''`, as such code always did.
8834
9118
  */
8835
9119
  async clearSecrets(keys, nodeId) {
9120
+ await this.clearSecretKeys(keys, nodeId);
9121
+ }
9122
+ async clearSecretKeys(keys, nodeId, cap) {
8836
9123
  const tree = this.addonSecretTree();
8837
9124
  const keychain = this._ctx?.secrets !== void 0;
8838
9125
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8845,25 +9132,27 @@ var BaseAddon = class {
8845
9132
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8846
9133
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8847
9134
  }
8848
- await this.applyGlobalPatch(patch, nodeId);
9135
+ await this.applyGlobalPatch(patch, nodeId, cap);
8849
9136
  }
8850
- async applyGlobalPatch(barePatch, nodeId) {
8851
- await this.loadScopedSchemas();
8852
- const keys = this.perNodeKeys();
9137
+ /** Resolves `true` when the write changed stored values. */
9138
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9139
+ await this.loadScopedSchemas(cap);
9140
+ const keys = this.perNodeKeys(cap);
8853
9141
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8854
9142
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8855
9143
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8856
9144
  const handle = this._secretSettings;
8857
9145
  if (handle !== null) {
8858
9146
  const { changed } = await handle.writeAddonStore(scoped);
8859
- if (!changed) return;
9147
+ if (!changed) return false;
8860
9148
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8861
- if (target !== localNode) return;
9149
+ if (target !== localNode) return true;
8862
9150
  await this.resolveConfig();
8863
9151
  await this.onConfigChanged();
8864
9152
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8865
9153
  const legacy = this.globalSettingsSchema();
8866
9154
  this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9155
+ return true;
8867
9156
  }
8868
9157
  /**
8869
9158
  * The set of field keys the global settings schema declares `perNode: true`
@@ -9558,30 +9847,94 @@ function normalizeAddonInitResult(result) {
9558
9847
  if (Array.isArray(result)) return { providers: result };
9559
9848
  return result;
9560
9849
  }
9850
+ object({
9851
+ field: string().min(1),
9852
+ on: literal("advance")
9853
+ });
9561
9854
  /**
9562
- * The share-view scope kinds a share-link token can carry. The hub mints
9563
- * and validates them (`ShareViewGrantSchema` builds its
9564
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9855
+ * Output schema shared by the contribution + live methods.
9856
+ *
9857
+ * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
9858
+ * tabs[]) without importing from `../interfaces/config-ui.js` — a
9859
+ * concrete-but-lenient Zod object keeps tRPC output inference happy
9860
+ * (using `z.unknown()` here collapses unrelated router branches to
9861
+ * `unknown` when the generator re-inlines the huge AppRouter type).
9862
+ *
9863
+ * `.passthrough()` on sections/fields accepts whatever FormBuilder
9864
+ * extensions the caller adds (showWhen, displayScale, …) without
9865
+ * rebuilding every time a new field kind is introduced.
9565
9866
  */
9566
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9567
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9568
- var PeerBytesTicketSchema = object({
9569
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9570
- url: string().min(1),
9571
- /**
9572
- * The HOST node this URL means something on — the hub or a named agent,
9573
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9574
- * refuses `cross-node` by name when they differ, without dialling.
9575
- */
9576
- hostNodeId: string().min(1),
9577
- expiresAtMs: number().int().nonnegative(),
9578
- /**
9579
- * What the producer DECLARED the body to be, when it knows — `null` when it
9580
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9581
- * must be able to tell "the producer did not say" from "the body is empty".
9582
- */
9583
- declaredBytes: number().int().nonnegative().nullable()
9867
+ var ContributionSectionSchema = object({
9868
+ id: string(),
9869
+ title: string(),
9870
+ description: string().optional(),
9871
+ style: _enum(["card", "accordion"]).optional(),
9872
+ defaultCollapsed: boolean().optional(),
9873
+ columns: union([
9874
+ literal(1),
9875
+ literal(2),
9876
+ literal(3),
9877
+ literal(4)
9878
+ ]).optional(),
9879
+ tab: string().optional(),
9880
+ location: _enum([
9881
+ "settings",
9882
+ "top-tab",
9883
+ "hero"
9884
+ ]).optional(),
9885
+ order: number().optional(),
9886
+ fields: array(any())
9584
9887
  });
9888
+ object({
9889
+ tabs: array(object({
9890
+ id: string(),
9891
+ label: string(),
9892
+ icon: string(),
9893
+ order: number().optional()
9894
+ })).optional(),
9895
+ sections: array(ContributionSectionSchema)
9896
+ }).nullable();
9897
+ object({ deviceId: number() }), object({ deviceId: number() }), object({
9898
+ deviceId: number(),
9899
+ patch: record(string(), unknown())
9900
+ }), object({ success: literal(true) });
9901
+ object({ deviceId: number() }), unknown().nullable();
9902
+ function method(input, output, options) {
9903
+ return {
9904
+ input,
9905
+ output,
9906
+ kind: options?.kind ?? "query",
9907
+ ...options?.auth !== void 0 ? { auth: options.auth } : {},
9908
+ ...options?.access !== void 0 ? { access: options.access } : {},
9909
+ ...options?.role !== void 0 ? { role: options.role } : {},
9910
+ ...options?.caller !== void 0 ? { caller: options.caller } : {},
9911
+ timeoutMs: options?.timeoutMs
9912
+ };
9913
+ }
9914
+ /**
9915
+ * A wrapper/system-only method: served exclusively by the cap's system-level
9916
+ * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
9917
+ * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
9918
+ * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
9919
+ */
9920
+ function systemMethod(input, output, options) {
9921
+ return {
9922
+ ...method(input, output, options),
9923
+ systemOnly: true
9924
+ };
9925
+ }
9926
+ /**
9927
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9928
+ * on `InferProvider`. The `providerOptional: true` literal is what
9929
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9930
+ * for when this is the honest answer and when it is a soft stub.
9931
+ */
9932
+ function optionalMethod(input, output, options) {
9933
+ return {
9934
+ ...method(input, output, options),
9935
+ providerOptional: true
9936
+ };
9937
+ }
9585
9938
  function defOf(schema) {
9586
9939
  const zodBag = Reflect.get(schema, "_zod");
9587
9940
  if (typeof zodBag !== "object" || zodBag === null) return {};
@@ -9838,94 +10191,30 @@ function defineEntryKind(name, spec) {
9838
10191
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9839
10192
  return defined;
9840
10193
  }
9841
- object({
9842
- field: string().min(1),
9843
- on: literal("advance")
9844
- });
9845
10194
  /**
9846
- * Output schema shared by the contribution + live methods.
9847
- *
9848
- * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
9849
- * tabs[]) without importing from `../interfaces/config-ui.js` — a
9850
- * concrete-but-lenient Zod object keeps tRPC output inference happy
9851
- * (using `z.unknown()` here collapses unrelated router branches to
9852
- * `unknown` when the generator re-inlines the huge AppRouter type).
9853
- *
9854
- * `.passthrough()` on sections/fields accepts whatever FormBuilder
9855
- * extensions the caller adds (showWhen, displayScale, …) without
9856
- * rebuilding every time a new field kind is introduced.
10195
+ * The share-view scope kinds a share-link token can carry. The hub mints
10196
+ * and validates them (`ShareViewGrantSchema` builds its
10197
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9857
10198
  */
9858
- var ContributionSectionSchema = object({
9859
- id: string(),
9860
- title: string(),
9861
- description: string().optional(),
9862
- style: _enum(["card", "accordion"]).optional(),
9863
- defaultCollapsed: boolean().optional(),
9864
- columns: union([
9865
- literal(1),
9866
- literal(2),
9867
- literal(3),
9868
- literal(4)
9869
- ]).optional(),
9870
- tab: string().optional(),
9871
- location: _enum([
9872
- "settings",
9873
- "top-tab",
9874
- "hero"
9875
- ]).optional(),
9876
- order: number().optional(),
9877
- fields: array(any())
10199
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
10200
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
10201
+ var PeerBytesTicketSchema = object({
10202
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
10203
+ url: string().min(1),
10204
+ /**
10205
+ * The HOST node this URL means something on — the hub or a named agent,
10206
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
10207
+ * refuses `cross-node` by name when they differ, without dialling.
10208
+ */
10209
+ hostNodeId: string().min(1),
10210
+ expiresAtMs: number().int().nonnegative(),
10211
+ /**
10212
+ * What the producer DECLARED the body to be, when it knows — `null` when it
10213
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
10214
+ * must be able to tell "the producer did not say" from "the body is empty".
10215
+ */
10216
+ declaredBytes: number().int().nonnegative().nullable()
9878
10217
  });
9879
- object({
9880
- tabs: array(object({
9881
- id: string(),
9882
- label: string(),
9883
- icon: string(),
9884
- order: number().optional()
9885
- })).optional(),
9886
- sections: array(ContributionSectionSchema)
9887
- }).nullable();
9888
- object({ deviceId: number() }), object({ deviceId: number() }), object({
9889
- deviceId: number(),
9890
- patch: record(string(), unknown())
9891
- }), object({ success: literal(true) });
9892
- object({ deviceId: number() }), unknown().nullable();
9893
- function method(input, output, options) {
9894
- return {
9895
- input,
9896
- output,
9897
- kind: options?.kind ?? "query",
9898
- ...options?.auth !== void 0 ? { auth: options.auth } : {},
9899
- ...options?.access !== void 0 ? { access: options.access } : {},
9900
- ...options?.role !== void 0 ? { role: options.role } : {},
9901
- ...options?.caller !== void 0 ? { caller: options.caller } : {},
9902
- timeoutMs: options?.timeoutMs
9903
- };
9904
- }
9905
- /**
9906
- * A wrapper/system-only method: served exclusively by the cap's system-level
9907
- * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
9908
- * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
9909
- * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
9910
- */
9911
- function systemMethod(input, output, options) {
9912
- return {
9913
- ...method(input, output, options),
9914
- systemOnly: true
9915
- };
9916
- }
9917
- /**
9918
- * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9919
- * on `InferProvider`. The `providerOptional: true` literal is what
9920
- * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9921
- * for when this is the honest answer and when it is a soft stub.
9922
- */
9923
- function optionalMethod(input, output, options) {
9924
- return {
9925
- ...method(input, output, options),
9926
- providerOptional: true
9927
- };
9928
- }
9929
10218
  var READING_QUANTITIES = [
9930
10219
  "temperature",
9931
10220
  "pressure",
@@ -10504,7 +10793,6 @@ var CAP_NAMES = Object.freeze([
10504
10793
  "climate-control",
10505
10794
  "color",
10506
10795
  "composer",
10507
- "connection-test",
10508
10796
  "connectivity",
10509
10797
  "consumables",
10510
10798
  "contact",
@@ -10594,7 +10882,6 @@ var CAP_NAMES = Object.freeze([
10594
10882
  "notifier",
10595
10883
  "numeric-sensor",
10596
10884
  "oauth-integration",
10597
- "oidc-connections",
10598
10885
  "osd",
10599
10886
  "osd-manager",
10600
10887
  "pet-feeder",
@@ -12486,8 +12773,7 @@ Object.freeze({
12486
12773
  "device-provider": { "reloadDevice": 18e4 },
12487
12774
  "llm": {
12488
12775
  "generate": 6e5,
12489
- "generateVision": 6e5,
12490
- "testProfile": 6e5
12776
+ "generateVision": 6e5
12491
12777
  },
12492
12778
  "llm-runtime": {
12493
12779
  "complete": 6e5,
@@ -12524,8 +12810,7 @@ Object.freeze({
12524
12810
  "offerArtifact": 3e5,
12525
12811
  "readResult": 3e5,
12526
12812
  "submitJob": 18e5,
12527
- "uploadDataset": 72e5,
12528
- "validateCredentials": 3e4
12813
+ "uploadDataset": 72e5
12529
12814
  }
12530
12815
  });
12531
12816
  object({
@@ -13940,6 +14225,17 @@ function composeInstanceKinds(parts, base) {
13940
14225
  if (part === void 0) throw new UnknownInstanceKindError(kindId);
13941
14226
  return part;
13942
14227
  };
14228
+ /**
14229
+ * The part a DRAFT test runs on. A named kind is that kind; an unnamed one is the provider's only
14230
+ * testable kind. Several testable kinds and none named is a typed refusal that lists them, never a throw.
14231
+ */
14232
+ const draftPart = (kindId) => {
14233
+ if (kindId !== void 0) return parts.find((candidate) => candidate.kind.id === kindId) ?? { error: `Unknown instance kind '${kindId}'` };
14234
+ const testable = parts.filter((part) => part.kind.canTest !== false);
14235
+ const only = testable.length === 1 ? testable[0] : parts.length === 1 ? parts[0] : void 0;
14236
+ if (only !== void 0) return only;
14237
+ return { error: `This provider has several kinds: name one of ${(testable.length > 0 ? testable : parts).map((part) => part.kind.id).join(", ")}` };
14238
+ };
13943
14239
  const holderOf = async (id) => {
13944
14240
  for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13945
14241
  throw new UnknownInstanceError(id);
@@ -13975,7 +14271,13 @@ function composeInstanceKinds(parts, base) {
13975
14271
  setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13976
14272
  deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13977
14273
  testInstance: async (input) => {
13978
- const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
14274
+ const resolved = input.id === null ? draftPart(input.kind) : await holderOf(input.id);
14275
+ if ("error" in resolved) return {
14276
+ success: false,
14277
+ status: "inconclusive",
14278
+ error: resolved.error
14279
+ };
14280
+ const test = resolved.provider.testInstance;
13979
14281
  if (test === void 0) return {
13980
14282
  success: false,
13981
14283
  status: "inconclusive",
@@ -13985,6 +14287,414 @@ function composeInstanceKinds(parts, base) {
13985
14287
  }
13986
14288
  };
13987
14289
  }
14290
+ /**
14291
+ * notification-output — canonical, capability-gated notification delivery.
14292
+ *
14293
+ * Apprise-derived model (see
14294
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
14295
+ * callers emit ONE canonical `Notification`; each provider declares a
14296
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
14297
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
14298
+ * message to what the kind supports — callers never special-case a service.
14299
+ *
14300
+ * DESIGN DECISIONS (locked):
14301
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14302
+ * providers serve their targets as `integration-instances` records, one
14303
+ * record kind per target kind, and the admin lists, adds, edits, switches
14304
+ * and deletes them through that cap. This cap keeps what an instance cannot
14305
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14306
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14307
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14308
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14309
+ * cap-mount auto-`concatCollection`-fans them across every registered
14310
+ * provider so one catalog is routable. `send` / `testTarget` /
14311
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14312
+ * collection router extracts from the call input.
14313
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14314
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14315
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
14316
+ * base64 fallback needed.
14317
+ *
14318
+ * TODO (deferred, closed-set change — separate decision): add
14319
+ * `providerKind: 'notify'` so notification providers surface on the unified
14320
+ * admin "Integrations" page.
14321
+ */
14322
+ /**
14323
+ * Zentik-derived typed-media enum — the superset across every kind. Each
14324
+ * adapter picks what it supports and the degrade engine filters the rest.
14325
+ */
14326
+ var AttachmentMediaTypeSchema = _enum([
14327
+ "image",
14328
+ "video",
14329
+ "gif",
14330
+ "audio",
14331
+ "icon"
14332
+ ]);
14333
+ /**
14334
+ * A single attachment. Exactly one of `url` (remote source, most adapters
14335
+ * prefer this) or `bytes` (inline source; required for Pushover-style
14336
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
14337
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
14338
+ */
14339
+ var AttachmentSchema = object({
14340
+ mediaType: AttachmentMediaTypeSchema,
14341
+ url: string().optional(),
14342
+ bytes: _instanceof(Uint8Array).optional(),
14343
+ mime: string().optional(),
14344
+ name: string().optional()
14345
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
14346
+ var NotificationFormatSchema = _enum([
14347
+ "text",
14348
+ "markdown",
14349
+ "html"
14350
+ ]);
14351
+ /**
14352
+ * The CLOSED icon vocabulary an action button may use.
14353
+ *
14354
+ * A closed set, not a free string, and that is the whole point: an arbitrary
14355
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
14356
+ * notices — the same class of gap as a zone vocabulary nothing produced
14357
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
14358
+ * declares `actionIcons: false` and the degrade engine strips the field.
14359
+ *
14360
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
14361
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
14362
+ * differently.
14363
+ *
14364
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
14365
+ *
14366
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
14367
+ * the whole set onto its own renderer's vocabulary through a
14368
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
14369
+ * fallback, so adding a member here fails every adapter's build until someone
14370
+ * decides its glyph, which is the only place that decision can be made
14371
+ * honestly.
14372
+ *
14373
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
14374
+ * `disarm` straight through; iOS feeds that string to
14375
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
14376
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
14377
+ * mapping, and "the field is documented" is not "the value renders".
14378
+ *
14379
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
14380
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
14381
+ * so an addon that emits a token the running hub does not know does not lose an
14382
+ * icon — its whole `send` fails Zod validation and the notification never
14383
+ * arrives. Never emit a new token from an addon before the train carrying it.
14384
+ */
14385
+ var NotificationActionIconSchema = _enum([
14386
+ "acknowledge",
14387
+ "dismiss",
14388
+ "silence",
14389
+ "snooze",
14390
+ "view",
14391
+ "play",
14392
+ "open",
14393
+ "close",
14394
+ "lock",
14395
+ "unlock",
14396
+ "arm",
14397
+ "arm-home",
14398
+ "arm-away",
14399
+ "arm-night",
14400
+ "disarm",
14401
+ "light",
14402
+ "alert",
14403
+ "camera"
14404
+ ]);
14405
+ /** A single tap-through action button. */
14406
+ var NotificationActionSchema = object({
14407
+ id: string(),
14408
+ label: string(),
14409
+ url: string().optional(),
14410
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14411
+ icon: NotificationActionIconSchema.optional(),
14412
+ /**
14413
+ * Renders in a warning style where the notifier supports it.
14414
+ *
14415
+ * A HINT, never a gate. The callback's authority is its token and nothing
14416
+ * else — see `notification-center/action-token.ts` for what that does and
14417
+ * does not buy.
14418
+ */
14419
+ destructive: boolean().optional(),
14420
+ /**
14421
+ * How the tap should REACH the url.
14422
+ *
14423
+ * `navigate` (absent, and every button authored before this field) opens it:
14424
+ * the phone leaves the notification and shows whatever the callback returns.
14425
+ * That is right for a button whose answer the operator wants to read.
14426
+ *
14427
+ * `background` fires it as a POST and stays put. It exists for the buttons
14428
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14429
+ * an answer to the notification, and being thrown into a browser tab to
14430
+ * confirm it costs more attention than the notification did. A backend that
14431
+ * cannot do a background call renders it as an ordinary link (the adapters
14432
+ * fall back rather than dropping the button), so this is a preference, never
14433
+ * a requirement.
14434
+ */
14435
+ mode: _enum(["navigate", "background"]).optional()
14436
+ });
14437
+ /** A delivery parameter value: what a notifier declared it takes. */
14438
+ var DeliveryParamValueSchema = union([
14439
+ string(),
14440
+ number(),
14441
+ boolean()
14442
+ ]);
14443
+ /**
14444
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14445
+ * addresses a target of that kind may set it; a kind that declares nothing is
14446
+ * never handed one. The admin UI derives its control from this descriptor —
14447
+ * no editor knows a service by name.
14448
+ *
14449
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14450
+ * run on the addon cap path).
14451
+ */
14452
+ var DeliveryParamSchema = object({
14453
+ key: string().min(1),
14454
+ label: string(),
14455
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14456
+ labelKey: string().optional(),
14457
+ type: _enum([
14458
+ "enum",
14459
+ "number",
14460
+ "boolean",
14461
+ "string"
14462
+ ]),
14463
+ /** `enum` only: the closed set of accepted values. */
14464
+ options: array(object({
14465
+ value: string(),
14466
+ label: string()
14467
+ })).optional(),
14468
+ /** `number` only. */
14469
+ min: number().optional(),
14470
+ max: number().optional(),
14471
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14472
+ default: DeliveryParamValueSchema.optional()
14473
+ });
14474
+ /**
14475
+ * The canonical notification. `body` is the only hard field (Apprise model).
14476
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14477
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14478
+ * the adapter maps this ordinal onto its native level. `level?` is an
14479
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14480
+ * `priority` for that one target.
14481
+ */
14482
+ var NotificationSchema = object({
14483
+ body: string(),
14484
+ title: string().optional(),
14485
+ format: NotificationFormatSchema.default("text"),
14486
+ priority: number().int().min(1).max(5).default(3),
14487
+ level: string().optional(),
14488
+ attachments: array(AttachmentSchema).optional(),
14489
+ clickUrl: string().optional(),
14490
+ actions: array(NotificationActionSchema).optional(),
14491
+ sound: string().optional(),
14492
+ ttl: number().optional(),
14493
+ tag: string().optional(),
14494
+ deviceId: number().optional(),
14495
+ eventId: string().optional(),
14496
+ metadata: record(string(), unknown()).optional(),
14497
+ /**
14498
+ * Values for the parameters the target's kind DECLARES in
14499
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14500
+ * declaration by the sender; a notifier reads only the keys it declared.
14501
+ */
14502
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14503
+ });
14504
+ /** One declared native severity/priority level for a kind. */
14505
+ var TargetKindLevelSchema = object({
14506
+ id: string(),
14507
+ label: string(),
14508
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14509
+ ordinal: number().int().min(1).max(5).nullable(),
14510
+ flags: object({
14511
+ critical: boolean().optional(),
14512
+ silent: boolean().optional(),
14513
+ noPush: boolean().optional()
14514
+ }).optional(),
14515
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14516
+ requires: array(string()).optional(),
14517
+ description: string().optional()
14518
+ });
14519
+ /** The full capability block consulted before dispatch. */
14520
+ var TargetKindCapsSchema = object({
14521
+ attachments: object({
14522
+ mediaTypes: array(AttachmentMediaTypeSchema),
14523
+ mode: _enum([
14524
+ "url",
14525
+ "bytes",
14526
+ "both"
14527
+ ]),
14528
+ max: number().int().nonnegative(),
14529
+ maxBytes: number().int().positive().optional()
14530
+ }),
14531
+ /** Max action buttons (0 = none). */
14532
+ actions: number().int().nonnegative(),
14533
+ /**
14534
+ * Whether this kind renders a per-action ICON.
14535
+ *
14536
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14537
+ * run on the addon cap path — three production failures in one day taught
14538
+ * this repo that once. Absent is read as false by the degrade engine, which
14539
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14540
+ * assumed and dropped costs the operator's trust in the field.
14541
+ */
14542
+ actionIcons: boolean().optional(),
14543
+ levels: array(TargetKindLevelSchema),
14544
+ format: array(NotificationFormatSchema),
14545
+ clickUrl: boolean(),
14546
+ sound: boolean(),
14547
+ ttl: boolean(),
14548
+ bodyMaxLen: number().int().positive()
14549
+ });
14550
+ /**
14551
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14552
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14553
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14554
+ * the union is large and not meant for runtime validation here; the exported
14555
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14556
+ */
14557
+ var ConfigSchemaPassthrough$3 = unknown();
14558
+ var TargetKindSchema = object({
14559
+ kind: string(),
14560
+ label: string(),
14561
+ icon: string(),
14562
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
14563
+ addonId: string(),
14564
+ /**
14565
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
14566
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
14567
+ * when the addon bundles no icon for that kind — the client then falls back
14568
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
14569
+ *
14570
+ * Root-relative on purpose: it resolves against whatever origin serves a web
14571
+ * client, and a native client joins it onto its own hub base.
14572
+ *
14573
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
14574
+ * field that survived only because the runtime cap-router forwards provider
14575
+ * output verbatim — so every consumer had to re-declare it by hand to stop
14576
+ * its own Zod parse from stripping it, and the whole arrangement would have
14577
+ * broken silently the moment output validation was tightened anywhere.
14578
+ */
14579
+ iconUrl: string().optional(),
14580
+ /**
14581
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
14582
+ *
14583
+ * The server knows this and therefore says it, because the client cannot
14584
+ * safely guess: a React-Native client renders SVG and raster through two
14585
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
14586
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
14587
+ * placeholder glyph for every vector icon while the web build looked fine.
14588
+ *
14589
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
14590
+ * not been updated — a client that cannot determine the type should prefer
14591
+ * its raster path, which is the safe default for an unknown image.
14592
+ */
14593
+ iconMediaType: string().optional(),
14594
+ configSchema: ConfigSchemaPassthrough$3,
14595
+ /**
14596
+ * The config keys that decide WHERE this kind's secrets are sent (server,
14597
+ * base URL, instance, chat, recipient). A save that changes any of them
14598
+ * while leaving a secret redacted is refused by the provider (D634), so an
14599
+ * editor uses this to ask for the secret on exactly those edits instead of
14600
+ * on every edit.
14601
+ *
14602
+ * Optional: absent from a provider that holds no secret (the Home Assistant
14603
+ * kinds) and from a provider older than this field — an editor that finds
14604
+ * it absent must assume ANY edit may need the secret.
14605
+ */
14606
+ destinationKeys: array(string()).optional(),
14607
+ supportsDiscovery: boolean(),
14608
+ /**
14609
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
14610
+ * Absent = none; the rule editor then offers nothing.
14611
+ */
14612
+ deliveryParams: array(DeliveryParamSchema).optional(),
14613
+ caps: TargetKindCapsSchema
14614
+ });
14615
+ /**
14616
+ * A target as a provider's own code sees it (the notification-center bridge, the
14617
+ * send path): the flat record body split into the name, the switch and the
14618
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14619
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14620
+ * secret to the UI.
14621
+ */
14622
+ var TargetSchema = object({
14623
+ id: string(),
14624
+ name: string(),
14625
+ kind: string(),
14626
+ addonId: string(),
14627
+ enabled: boolean(),
14628
+ config: record(string(), unknown())
14629
+ });
14630
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
14631
+ var DiscoveredTargetSchema = object({
14632
+ kind: string(),
14633
+ suggestedName: string(),
14634
+ config: record(string(), unknown())
14635
+ });
14636
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
14637
+ var RenderedAsSchema = object({
14638
+ level: string(),
14639
+ format: NotificationFormatSchema,
14640
+ attachmentsSent: number().int().nonnegative(),
14641
+ actionsSent: number().int().nonnegative(),
14642
+ truncated: boolean(),
14643
+ dropped: array(string())
14644
+ });
14645
+ var SendResultSchema = object({
14646
+ success: boolean(),
14647
+ error: string().optional(),
14648
+ renderedAs: RenderedAsSchema.optional()
14649
+ });
14650
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
14651
+ var TestResultSchema = SendResultSchema;
14652
+ /**
14653
+ * Pre-save credential probe against a partial (unsaved) target config.
14654
+ * `resolvedUrl` never includes secrets (token stripped from path).
14655
+ */
14656
+ var ProbeTargetConfigResultSchema = object({
14657
+ ok: boolean(),
14658
+ resolvedUrl: string(),
14659
+ httpStatus: number().int().optional(),
14660
+ stateInstance: string().optional(),
14661
+ error: string().optional()
14662
+ });
14663
+ method(object({}), array(TargetKindSchema)), method(object({
14664
+ kind: string(),
14665
+ config: record(string(), unknown()).optional(),
14666
+ /**
14667
+ * When editing / duplicating, the blank password field never round-trips
14668
+ * the stored token. Pass the source target id so the provider can restore
14669
+ * secrets server-side before dialling.
14670
+ */
14671
+ targetId: string().optional()
14672
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
14673
+ kind: string(),
14674
+ config: record(string(), unknown()),
14675
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
14676
+ targetId: string().optional()
14677
+ }), ProbeTargetConfigResultSchema, {
14678
+ kind: "mutation",
14679
+ auth: "admin"
14680
+ }), method(object({
14681
+ targetId: string(),
14682
+ notification: NotificationSchema
14683
+ }), SendResultSchema, {
14684
+ kind: "mutation",
14685
+ auth: "admin"
14686
+ }), method(object({
14687
+ targetId: string(),
14688
+ sample: NotificationSchema.optional()
14689
+ }), TestResultSchema, {
14690
+ kind: "mutation",
14691
+ auth: "admin"
14692
+ });
14693
+ looseObject({
14694
+ name: string().min(1),
14695
+ enabled: boolean().optional()
14696
+ });
14697
+ TargetSchema.omit({ id: true });
13988
14698
  var EncodeProfileSchema = object({
13989
14699
  video: object({
13990
14700
  codec: _enum([
@@ -22202,7 +22912,6 @@ var SYSTEM_CAP_VOCABULARY = {
22202
22912
  "calendar-source": system(INTERNAL),
22203
22913
  calendars: system("calendars feed schedules and conditions, not device actions"),
22204
22914
  composer: system("device composition — an authoring tool, not an action"),
22205
- "connection-test": system(INTERNAL),
22206
22915
  "core-blocks": system(INTERNAL),
22207
22916
  "custom-model-registry": system(ML),
22208
22917
  "data-store-provider": system(INTERNAL),
@@ -22247,7 +22956,6 @@ var SYSTEM_CAP_VOCABULARY = {
22247
22956
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22248
22957
  "notification-rules": system("notification rules — authored on their own page"),
22249
22958
  "oauth-integration": system(IDENTITY),
22250
- "oidc-connections": system(IDENTITY),
22251
22959
  "osd-manager": system(PIPELINE),
22252
22960
  "pipeline-executor": system(PIPELINE),
22253
22961
  "pipeline-orchestrator": system(PIPELINE),
@@ -44119,427 +44827,6 @@ var UnitWarningSchema = object({
44119
44827
  assumedUnit: string(),
44120
44828
  message: string()
44121
44829
  });
44122
- /**
44123
- * notification-output — canonical, capability-gated notification delivery.
44124
- *
44125
- * Apprise-derived model (see
44126
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44127
- * callers emit ONE canonical `Notification`; each provider declares a
44128
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44129
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44130
- * message to what the kind supports — callers never special-case a service.
44131
- *
44132
- * DESIGN DECISIONS (locked):
44133
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44134
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44135
- * cap. Rationale: the admin UI needs one uniform surface across the
44136
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44137
- * alternative would fork the UI per addon and cannot host the
44138
- * discovery→adopt flow.
44139
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44140
- * the generated cap-mount auto-`concatCollection`-fans them across every
44141
- * registered provider (notifiers addon + HA addon) so one catalog is
44142
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44143
- * `addonId` the generated collection router extracts from the call input.
44144
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44145
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44146
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44147
- * base64 fallback needed.
44148
- *
44149
- * TODO (deferred, closed-set change — separate decision): add
44150
- * `providerKind: 'notify'` so notification providers surface on the unified
44151
- * admin "Integrations" page.
44152
- */
44153
- /**
44154
- * Zentik-derived typed-media enum — the superset across every kind. Each
44155
- * adapter picks what it supports and the degrade engine filters the rest.
44156
- */
44157
- var AttachmentMediaTypeSchema = _enum([
44158
- "image",
44159
- "video",
44160
- "gif",
44161
- "audio",
44162
- "icon"
44163
- ]);
44164
- /**
44165
- * A single attachment. Exactly one of `url` (remote source, most adapters
44166
- * prefer this) or `bytes` (inline source; required for Pushover-style
44167
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44168
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44169
- */
44170
- var AttachmentSchema = object({
44171
- mediaType: AttachmentMediaTypeSchema,
44172
- url: string().optional(),
44173
- bytes: _instanceof(Uint8Array).optional(),
44174
- mime: string().optional(),
44175
- name: string().optional()
44176
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44177
- var NotificationFormatSchema = _enum([
44178
- "text",
44179
- "markdown",
44180
- "html"
44181
- ]);
44182
- /**
44183
- * The CLOSED icon vocabulary an action button may use.
44184
- *
44185
- * A closed set, not a free string, and that is the whole point: an arbitrary
44186
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44187
- * notices — the same class of gap as a zone vocabulary nothing produced
44188
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44189
- * declares `actionIcons: false` and the degrade engine strips the field.
44190
- *
44191
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44192
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44193
- * differently.
44194
- *
44195
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44196
- *
44197
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44198
- * the whole set onto its own renderer's vocabulary through a
44199
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44200
- * fallback, so adding a member here fails every adapter's build until someone
44201
- * decides its glyph, which is the only place that decision can be made
44202
- * honestly.
44203
- *
44204
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44205
- * `disarm` straight through; iOS feeds that string to
44206
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44207
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44208
- * mapping, and "the field is documented" is not "the value renders".
44209
- *
44210
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44211
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44212
- * so an addon that emits a token the running hub does not know does not lose an
44213
- * icon — its whole `send` fails Zod validation and the notification never
44214
- * arrives. Never emit a new token from an addon before the train carrying it.
44215
- */
44216
- var NotificationActionIconSchema = _enum([
44217
- "acknowledge",
44218
- "dismiss",
44219
- "silence",
44220
- "snooze",
44221
- "view",
44222
- "play",
44223
- "open",
44224
- "close",
44225
- "lock",
44226
- "unlock",
44227
- "arm",
44228
- "arm-home",
44229
- "arm-away",
44230
- "arm-night",
44231
- "disarm",
44232
- "light",
44233
- "alert",
44234
- "camera"
44235
- ]);
44236
- /** A single tap-through action button. */
44237
- var NotificationActionSchema = object({
44238
- id: string(),
44239
- label: string(),
44240
- url: string().optional(),
44241
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44242
- icon: NotificationActionIconSchema.optional(),
44243
- /**
44244
- * Renders in a warning style where the notifier supports it.
44245
- *
44246
- * A HINT, never a gate. The callback's authority is its token and nothing
44247
- * else — see `notification-center/action-token.ts` for what that does and
44248
- * does not buy.
44249
- */
44250
- destructive: boolean().optional(),
44251
- /**
44252
- * How the tap should REACH the url.
44253
- *
44254
- * `navigate` (absent, and every button authored before this field) opens it:
44255
- * the phone leaves the notification and shows whatever the callback returns.
44256
- * That is right for a button whose answer the operator wants to read.
44257
- *
44258
- * `background` fires it as a POST and stays put. It exists for the buttons
44259
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44260
- * an answer to the notification, and being thrown into a browser tab to
44261
- * confirm it costs more attention than the notification did. A backend that
44262
- * cannot do a background call renders it as an ordinary link (the adapters
44263
- * fall back rather than dropping the button), so this is a preference, never
44264
- * a requirement.
44265
- */
44266
- mode: _enum(["navigate", "background"]).optional()
44267
- });
44268
- /** A delivery parameter value: what a notifier declared it takes. */
44269
- var DeliveryParamValueSchema = union([
44270
- string(),
44271
- number(),
44272
- boolean()
44273
- ]);
44274
- /**
44275
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44276
- * addresses a target of that kind may set it; a kind that declares nothing is
44277
- * never handed one. The admin UI derives its control from this descriptor —
44278
- * no editor knows a service by name.
44279
- *
44280
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44281
- * run on the addon cap path).
44282
- */
44283
- var DeliveryParamSchema = object({
44284
- key: string().min(1),
44285
- label: string(),
44286
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44287
- labelKey: string().optional(),
44288
- type: _enum([
44289
- "enum",
44290
- "number",
44291
- "boolean",
44292
- "string"
44293
- ]),
44294
- /** `enum` only: the closed set of accepted values. */
44295
- options: array(object({
44296
- value: string(),
44297
- label: string()
44298
- })).optional(),
44299
- /** `number` only. */
44300
- min: number().optional(),
44301
- max: number().optional(),
44302
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
44303
- default: DeliveryParamValueSchema.optional()
44304
- });
44305
- /**
44306
- * The canonical notification. `body` is the only hard field (Apprise model).
44307
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
44308
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
44309
- * the adapter maps this ordinal onto its native level. `level?` is an
44310
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
44311
- * `priority` for that one target.
44312
- */
44313
- var NotificationSchema = object({
44314
- body: string(),
44315
- title: string().optional(),
44316
- format: NotificationFormatSchema.default("text"),
44317
- priority: number().int().min(1).max(5).default(3),
44318
- level: string().optional(),
44319
- attachments: array(AttachmentSchema).optional(),
44320
- clickUrl: string().optional(),
44321
- actions: array(NotificationActionSchema).optional(),
44322
- sound: string().optional(),
44323
- ttl: number().optional(),
44324
- tag: string().optional(),
44325
- deviceId: number().optional(),
44326
- eventId: string().optional(),
44327
- metadata: record(string(), unknown()).optional(),
44328
- /**
44329
- * Values for the parameters the target's kind DECLARES in
44330
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
44331
- * declaration by the sender; a notifier reads only the keys it declared.
44332
- */
44333
- delivery: record(string(), DeliveryParamValueSchema).optional()
44334
- });
44335
- /** One declared native severity/priority level for a kind. */
44336
- var TargetKindLevelSchema = object({
44337
- id: string(),
44338
- label: string(),
44339
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
44340
- ordinal: number().int().min(1).max(5).nullable(),
44341
- flags: object({
44342
- critical: boolean().optional(),
44343
- silent: boolean().optional(),
44344
- noPush: boolean().optional()
44345
- }).optional(),
44346
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
44347
- requires: array(string()).optional(),
44348
- description: string().optional()
44349
- });
44350
- /** The full capability block consulted before dispatch. */
44351
- var TargetKindCapsSchema = object({
44352
- attachments: object({
44353
- mediaTypes: array(AttachmentMediaTypeSchema),
44354
- mode: _enum([
44355
- "url",
44356
- "bytes",
44357
- "both"
44358
- ]),
44359
- max: number().int().nonnegative(),
44360
- maxBytes: number().int().positive().optional()
44361
- }),
44362
- /** Max action buttons (0 = none). */
44363
- actions: number().int().nonnegative(),
44364
- /**
44365
- * Whether this kind renders a per-action ICON.
44366
- *
44367
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
44368
- * run on the addon cap path — three production failures in one day taught
44369
- * this repo that once. Absent is read as false by the degrade engine, which
44370
- * is the safe direction: an icon that is not rendered costs nothing, an icon
44371
- * assumed and dropped costs the operator's trust in the field.
44372
- */
44373
- actionIcons: boolean().optional(),
44374
- levels: array(TargetKindLevelSchema),
44375
- format: array(NotificationFormatSchema),
44376
- clickUrl: boolean(),
44377
- sound: boolean(),
44378
- ttl: boolean(),
44379
- bodyMaxLen: number().int().positive()
44380
- });
44381
- /**
44382
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
44383
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
44384
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
44385
- * the union is large and not meant for runtime validation here; the exported
44386
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
44387
- */
44388
- var ConfigSchemaPassthrough$4 = unknown();
44389
- var TargetKindSchema = object({
44390
- kind: string(),
44391
- label: string(),
44392
- icon: string(),
44393
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
44394
- addonId: string(),
44395
- /**
44396
- * URL of the kind's bundled BRAND icon, served by the providing addon over
44397
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
44398
- * when the addon bundles no icon for that kind — the client then falls back
44399
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
44400
- *
44401
- * Root-relative on purpose: it resolves against whatever origin serves a web
44402
- * client, and a native client joins it onto its own hub base.
44403
- *
44404
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
44405
- * field that survived only because the runtime cap-router forwards provider
44406
- * output verbatim — so every consumer had to re-declare it by hand to stop
44407
- * its own Zod parse from stripping it, and the whole arrangement would have
44408
- * broken silently the moment output validation was tightened anywhere.
44409
- */
44410
- iconUrl: string().optional(),
44411
- /**
44412
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
44413
- *
44414
- * The server knows this and therefore says it, because the client cannot
44415
- * safely guess: a React-Native client renders SVG and raster through two
44416
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
44417
- * not decode SVG on iOS/Android), so without this it silently fell back to a
44418
- * placeholder glyph for every vector icon while the web build looked fine.
44419
- *
44420
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
44421
- * not been updated — a client that cannot determine the type should prefer
44422
- * its raster path, which is the safe default for an unknown image.
44423
- */
44424
- iconMediaType: string().optional(),
44425
- configSchema: ConfigSchemaPassthrough$4,
44426
- /**
44427
- * The config keys that decide WHERE this kind's secrets are sent (server,
44428
- * base URL, instance, chat, recipient). A save that changes any of them
44429
- * while leaving a secret redacted is refused by the provider (D634), so an
44430
- * editor uses this to ask for the secret on exactly those edits instead of
44431
- * on every edit.
44432
- *
44433
- * Optional: absent from a provider that holds no secret (the Home Assistant
44434
- * kinds) and from a provider older than this field — an editor that finds
44435
- * it absent must assume ANY edit may need the secret.
44436
- */
44437
- destinationKeys: array(string()).optional(),
44438
- supportsDiscovery: boolean(),
44439
- /**
44440
- * Extra delivery parameters a rule may set for a target of this kind (D938).
44441
- * Absent = none; the rule editor then offers nothing.
44442
- */
44443
- deliveryParams: array(DeliveryParamSchema).optional(),
44444
- caps: TargetKindCapsSchema
44445
- });
44446
- /**
44447
- * A persisted target. `config` holds secrets; providers REDACT secret fields
44448
- * (return a presence marker only) when serving `listTargets` — never
44449
- * round-trip a stored secret to the UI.
44450
- */
44451
- var TargetSchema = object({
44452
- id: string(),
44453
- name: string(),
44454
- kind: string(),
44455
- addonId: string(),
44456
- enabled: boolean(),
44457
- config: record(string(), unknown())
44458
- });
44459
- /** A discovery-surfaced candidate (config is partial + non-secret). */
44460
- var DiscoveredTargetSchema = object({
44461
- kind: string(),
44462
- suggestedName: string(),
44463
- config: record(string(), unknown())
44464
- });
44465
- /** The degrade engine's report — what was resolved / dropped / degraded. */
44466
- var RenderedAsSchema = object({
44467
- level: string(),
44468
- format: NotificationFormatSchema,
44469
- attachmentsSent: number().int().nonnegative(),
44470
- actionsSent: number().int().nonnegative(),
44471
- truncated: boolean(),
44472
- dropped: array(string())
44473
- });
44474
- var SendResultSchema = object({
44475
- success: boolean(),
44476
- error: string().optional(),
44477
- renderedAs: RenderedAsSchema.optional()
44478
- });
44479
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
44480
- var TestResultSchema = SendResultSchema;
44481
- /**
44482
- * Pre-save credential probe against a partial (unsaved) target config.
44483
- * `resolvedUrl` never includes secrets (token stripped from path).
44484
- */
44485
- var ProbeTargetConfigResultSchema = object({
44486
- ok: boolean(),
44487
- resolvedUrl: string(),
44488
- httpStatus: number().int().optional(),
44489
- stateInstance: string().optional(),
44490
- error: string().optional()
44491
- });
44492
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
44493
- kind: string(),
44494
- config: record(string(), unknown()).optional(),
44495
- /**
44496
- * When editing / duplicating, the blank password field never round-trips
44497
- * the stored token. Pass the source target id so the provider can restore
44498
- * secrets server-side before dialling (same merge as upsert).
44499
- */
44500
- targetId: string().optional()
44501
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
44502
- kind: string(),
44503
- config: record(string(), unknown()),
44504
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44505
- targetId: string().optional()
44506
- }), ProbeTargetConfigResultSchema, {
44507
- kind: "mutation",
44508
- auth: "admin"
44509
- }), method(object({
44510
- targetId: string(),
44511
- notification: NotificationSchema
44512
- }), SendResultSchema, {
44513
- kind: "mutation",
44514
- auth: "admin"
44515
- }), method(object({
44516
- targetId: string(),
44517
- sample: NotificationSchema.optional()
44518
- }), TestResultSchema, {
44519
- kind: "mutation",
44520
- auth: "admin"
44521
- }), method(object({
44522
- target: TargetSchema,
44523
- /**
44524
- * Duplicate flow: create a NEW target id while copying secrets from an
44525
- * existing row. The UI never holds the stored token; blank / redacted
44526
- * password fields are restored from this source (D634 still applies if
44527
- * destinations diverge without a re-typed secret).
44528
- */
44529
- cloneSecretsFrom: string().optional()
44530
- }), TargetSchema, {
44531
- kind: "mutation",
44532
- auth: "admin"
44533
- }), method(object({ targetId: string() }), _void(), {
44534
- kind: "mutation",
44535
- auth: "admin"
44536
- }), method(object({
44537
- targetId: string(),
44538
- enabled: boolean()
44539
- }), _void(), {
44540
- kind: "mutation",
44541
- auth: "admin"
44542
- });
44543
44830
  _enum([
44544
44831
  "automation",
44545
44832
  "action",
@@ -44810,6 +45097,8 @@ var SettingsSchemaWithValuesSchema = object({
44810
45097
  });
44811
45098
  /** Patch object — keys are field names, values are the new field values. */
44812
45099
  var SettingsPatchSchema = record(string(), unknown());
45100
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45101
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
44813
45102
  /** Standard success response for update operations. */
44814
45103
  var SettingsUpdateResultSchema = object({ success: literal(true) });
44815
45104
  method(object({
@@ -44825,6 +45114,25 @@ method(object({
44825
45114
  kind: "mutation",
44826
45115
  auth: "admin"
44827
45116
  }), method(object({
45117
+ addonId: string(),
45118
+ cap: string(),
45119
+ scope: ScopedSettingsScopeSchema,
45120
+ nodeId: string().optional()
45121
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45122
+ addonId: string(),
45123
+ cap: string(),
45124
+ scope: ScopedSettingsScopeSchema,
45125
+ nodeId: string().optional(),
45126
+ patch: SettingsPatchSchema
45127
+ }), SettingsUpdateResultSchema, {
45128
+ kind: "mutation",
45129
+ auth: "admin"
45130
+ }), method(object({
45131
+ addonId: string(),
45132
+ cap: string(),
45133
+ scope: ScopedSettingsScopeSchema,
45134
+ nodeId: string().optional()
45135
+ }), record(string(), unknown()).nullable()), method(object({
44828
45136
  addonId: string(),
44829
45137
  nodeId: string().optional()
44830
45138
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
@@ -48417,37 +48725,6 @@ method(object({
48417
48725
  kind: "mutation",
48418
48726
  auth: "admin"
48419
48727
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
48420
- var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
48421
- object({
48422
- outcome: literal("validated"),
48423
- /** Round-trip of the sign-in, when the provider measured it. */
48424
- latencyMs: number().nonnegative().optional(),
48425
- /** Optional human detail worth showing next to the tick
48426
- * ("3 devices visible on this account"). */
48427
- detail: string().optional()
48428
- }).strict(),
48429
- object({
48430
- outcome: literal("rejected"),
48431
- error: string()
48432
- }).strict(),
48433
- object({
48434
- outcome: literal("inconclusive"),
48435
- error: string()
48436
- }).strict()
48437
- ]);
48438
- var ConnectionTestInputSchema = object({
48439
- /** Candidate integration settings, exactly as the create form collected them. */
48440
- settings: record(string(), unknown()) });
48441
- /**
48442
- * What the provider's test actually DOES, so the UI can say it in words before
48443
- * the operator presses the button ("Signs in to the Dreo cloud"). Purely
48444
- * descriptive — it never changes routing.
48445
- */
48446
- var ConnectionTestDescriptorSchema = object({ label: string() });
48447
- method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
48448
- kind: "mutation",
48449
- auth: "admin"
48450
- }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
48451
48728
  object({
48452
48729
  /** True when the upstream system considers the entity connected. */
48453
48730
  connected: boolean(),
@@ -54301,10 +54578,9 @@ var AvailableIntegrationTypeSchema = object({
54301
54578
  * locations" checkbox. Provider-declared in the addon manifest. */
54302
54579
  supportsLocationImport: boolean(),
54303
54580
  /**
54304
- * True when this integration DECLARES a pre-creation test (the
54305
- * `connection-test` cap, or a broker whose settings it stores). Drives the
54306
- * Test button: an integration that cannot be tested must say so up front
54307
- * rather than offering a button that always answers the same nonsense.
54581
+ * True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
54582
+ * D1016). An integration that cannot be tested must say so up front rather than offering a
54583
+ * button that always answers the same nonsense.
54308
54584
  */
54309
54585
  canTest: boolean(),
54310
54586
  existingInstances: array(object({
@@ -54358,7 +54634,9 @@ var CreateIntegrationInputSchema = object({
54358
54634
  info: record(string(), unknown()).optional(),
54359
54635
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54360
54636
  originStableId: string().optional(),
54361
- settings: record(string(), unknown()).optional()
54637
+ settings: record(string(), unknown()).optional(),
54638
+ /** The instance kind being created, for a provider with several; omitted = its only testable kind. */
54639
+ kind: string().min(1).optional()
54362
54640
  });
54363
54641
  var UpdateIntegrationInputSchema = object({
54364
54642
  id: string(),
@@ -54384,13 +54662,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54384
54662
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54385
54663
  kind: "mutation",
54386
54664
  auth: "admin"
54387
- }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54388
- addonId: string(),
54389
- settings: record(string(), unknown())
54390
- }), TestConnectionResultSchema, {
54391
- kind: "mutation",
54392
- auth: "admin"
54393
- });
54665
+ }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
54394
54666
  /**
54395
54667
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
54396
54668
  * (an account, a relay, a destination) as typed records (D985, D986). The addon's
@@ -54410,7 +54682,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54410
54682
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
54411
54683
  * clears it.
54412
54684
  */
54413
- var ConfigSchemaPassthrough$3 = custom();
54685
+ var ConfigSchemaPassthrough$2 = custom();
54414
54686
  var IntegrationInstanceIdSchema = string().min(1);
54415
54687
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54416
54688
  /**
@@ -54456,7 +54728,16 @@ var IntegrationInstanceKindSchema = object({
54456
54728
  /** Relative hub path to start an interactive account link of an instance of this kind. */
54457
54729
  connectPath: string().min(1).optional(),
54458
54730
  /** Relative hub path to disconnect that account. */
54459
- disconnectPath: string().min(1).optional()
54731
+ disconnectPath: string().min(1).optional(),
54732
+ /**
54733
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
54734
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
54735
+ * `connection` widgets write connection values, `model` widgets own the model choice.
54736
+ */
54737
+ widgets: array(object({
54738
+ id: string().min(1),
54739
+ placement: _enum(["connection", "model"])
54740
+ })).optional()
54460
54741
  });
54461
54742
  /**
54462
54743
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -54561,7 +54842,7 @@ var integrationInstancesCapability = {
54561
54842
  id: IntegrationInstanceIdSchema.nullable(),
54562
54843
  /** The kind of the create form (`id: null`) of a provider with several. */
54563
54844
  kind: string().min(1).optional()
54564
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54845
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
54565
54846
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54566
54847
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54567
54848
  /**
@@ -55512,12 +55793,12 @@ var LlmModelInfoSchema = object({
55512
55793
  });
55513
55794
  /**
55514
55795
  * The provider-specific part of a profile, validated by the provider that owns
55515
- * the kind (`upsertProfile` refuses what it does not accept). A field its
55796
+ * the kind (saving an instance refuses what it does not accept). A field its
55516
55797
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
55517
55798
  * keychain, never in the row, and reads back as the redaction marker.
55518
55799
  */
55519
55800
  var LlmProviderConfigSchema = record(string(), unknown());
55520
- var LlmProfileSchema = object({
55801
+ object({
55521
55802
  id: string(),
55522
55803
  name: string(),
55523
55804
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -55611,8 +55892,8 @@ var LlmProviderWidgetSchema = object({
55611
55892
  /** ConfigUISchema tree passed through untyped on the wire (the
55612
55893
  * notification-output `ConfigSchemaPassthrough` precedent at
55613
55894
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
55614
- var ConfigSchemaPassthrough$2 = unknown();
55615
- var LlmProfileKindDescriptorSchema = object({
55895
+ var ConfigSchemaPassthrough$1 = unknown();
55896
+ object({
55616
55897
  kind: LlmProfileKindSchema,
55617
55898
  label: string(),
55618
55899
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -55620,9 +55901,9 @@ var LlmProfileKindDescriptorSchema = object({
55620
55901
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
55621
55902
  addonId: string(),
55622
55903
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
55623
- connectionSchema: ConfigSchemaPassthrough$2,
55904
+ connectionSchema: ConfigSchemaPassthrough$1,
55624
55905
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
55625
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
55906
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
55626
55907
  /** The kind's defaults; a model from `listModels` may override any of them. */
55627
55908
  features: LlmModelFeaturesSchema,
55628
55909
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -55751,20 +56032,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
55751
56032
  }), method(object({
55752
56033
  addonId: string().optional(),
55753
56034
  requestId: string()
55754
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
55755
- kind: "mutation",
55756
- auth: "admin"
55757
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
55758
- kind: "mutation",
55759
- auth: "admin"
55760
- }), method(ProfileRefInputSchema, _void(), {
55761
- kind: "mutation",
55762
- auth: "admin"
55763
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
55764
- kind: "mutation",
55765
- auth: "admin",
55766
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
55767
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
56035
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55768
56036
  kind: "mutation",
55769
56037
  auth: "admin"
55770
56038
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -56759,20 +57027,7 @@ method(_void(), MeshStatusSchema), method(object({
56759
57027
  /** Optional hostname override the host should advertise once joined. */
56760
57028
  hostname: string().optional() }), object({
56761
57029
  /** Authentication URL the operator should open in a browser. */
56762
- loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() })), method(object({
56763
- /** Optional auth key — when provided, probes the key validity
56764
- * against the provider's API. Omit when already joined to
56765
- * just ping the daemon. */
56766
- authKey: string().optional() }), object({
56767
- ok: boolean(),
56768
- /** Provider-side identifier resolved by the probe (tailnet
56769
- * name for Tailscale, network id for ZeroTier, etc.). */
56770
- tenant: string().optional(),
56771
- /** Daemon binary version, when reachable. */
56772
- daemonVersion: string().optional(),
56773
- /** Human-readable error when `ok: false`. */
56774
- error: string().optional()
56775
- }), { kind: "mutation" });
57030
+ loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() }));
56776
57031
  object({
56777
57032
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
56778
57033
  lastFetchedAt: number()
@@ -62633,13 +62888,6 @@ var AddBrokerInputSchema = object({
62633
62888
  });
62634
62889
  var AddBrokerResultSchema = object({ id: string() });
62635
62890
  var IdInputSchema = object({ id: string() });
62636
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
62637
- ok: literal(true),
62638
- latencyMs: number()
62639
- }), object({
62640
- ok: literal(false),
62641
- error: string()
62642
- })]);
62643
62891
  var StartEmbeddedInputSchema = object({
62644
62892
  port: number().int().min(1).max(65535).default(1883),
62645
62893
  /** Allow anonymous connect (no username/password). Default: false. */
@@ -62662,9 +62910,6 @@ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnection
62662
62910
  }), method(IdInputSchema, _void(), {
62663
62911
  kind: "mutation",
62664
62912
  auth: "admin"
62665
- }), method(IdInputSchema, TestResultSchema$1, {
62666
- kind: "mutation",
62667
- auth: "admin"
62668
62913
  }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
62669
62914
  kind: "mutation",
62670
62915
  auth: "admin"
@@ -66440,61 +66685,6 @@ var OauthIntegrationDescriptorSchema = object({
66440
66685
  });
66441
66686
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
66442
66687
  /**
66443
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
66444
- *
66445
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
66446
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
66447
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
66448
- * immutable after create.
66449
- */
66450
- var ConfigSchemaPassthrough$1 = unknown();
66451
- var OidcConnectionSchema = object({
66452
- id: string().min(1),
66453
- displayName: string(),
66454
- icon: string(),
66455
- issuerUrl: string(),
66456
- clientId: string(),
66457
- /** Client id + secret present — never the secret value. */
66458
- configured: boolean(),
66459
- /**
66460
- * Values for the connection form schema. `clientSecret` is a keychain
66461
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
66462
- */
66463
- formValues: record(string(), unknown()).optional()
66464
- });
66465
- /**
66466
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
66467
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
66468
- */
66469
- var OidcConnectionUpdateInputSchema = object({
66470
- id: string().min(1),
66471
- displayName: string().min(1),
66472
- icon: string().optional(),
66473
- issuerUrl: string().optional(),
66474
- clientId: string().optional(),
66475
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
66476
- redirectUri: string().optional(),
66477
- scopes: string().optional(),
66478
- usernameClaim: _enum([
66479
- "preferred_username",
66480
- "email",
66481
- "sub"
66482
- ]).optional()
66483
- });
66484
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
66485
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
66486
- displayName: string().min(1)
66487
- }), object({ id: string() }), {
66488
- kind: "mutation",
66489
- auth: "admin"
66490
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
66491
- kind: "mutation",
66492
- auth: "admin"
66493
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
66494
- kind: "mutation",
66495
- auth: "admin"
66496
- });
66497
- /**
66498
66688
  * Generic on-screen-display (video overlay) cap. Each camera exposes
66499
66689
  * a firmware-defined list of overlays (channel name, timestamp,
66500
66690
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -72680,21 +72870,6 @@ method(object({
72680
72870
  */
72681
72871
  ttlSec: union([number().int().positive(), literal("never")]).optional()
72682
72872
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
72683
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
72684
- providerId: string().min(1),
72685
- displayName: string().min(1),
72686
- configSchema: unknown(),
72687
- nodeLocal: boolean(),
72688
- shouldSaveDiskSpace: literal(true),
72689
- minFreePercent: number().min(0).max(100)
72690
- }), object({
72691
- providerId: string().min(1),
72692
- displayName: string().min(1),
72693
- configSchema: unknown(),
72694
- nodeLocal: boolean(),
72695
- shouldSaveDiskSpace: literal(false),
72696
- minFreePercent: literal(null)
72697
- })]);
72698
72873
  method(object({
72699
72874
  location: StorageLocationRefSchema,
72700
72875
  relativePath: string()
@@ -72756,42 +72931,7 @@ method(object({
72756
72931
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
72757
72932
  kind: "mutation",
72758
72933
  auth: "admin"
72759
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
72760
- createdAt: true,
72761
- updatedAt: true
72762
- }), StorageLocationSchema, {
72763
- kind: "mutation",
72764
- auth: "admin"
72765
- }), method(object({
72766
- id: string(),
72767
- force: boolean().optional()
72768
- }), _void(), {
72769
- kind: "mutation",
72770
- auth: "admin"
72771
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
72772
- ok: boolean(),
72773
- error: string().optional()
72774
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
72775
- providerId: string(),
72776
- /**
72777
- * The location this config is an UNSAVED edit of, when there is one.
72778
- *
72779
- * `listLocations` replaces every declared secret with the redaction
72780
- * sentinel, so the edit modal's form state holds the sentinel for any
72781
- * credential the operator did not retype — and posting that here
72782
- * without a way to resolve it makes the provider try to authenticate
72783
- * as `__camstack_redacted__` and report the operator's own working
72784
- * password as wrong. Given this id, the orchestrator restores each
72785
- * sentinel from the stored config (same rule as `upsertLocation`)
72786
- * before dispatching. Omitted by the "Add location" wizard, where
72787
- * every value was typed just now and nothing is stored yet.
72788
- */
72789
- locationId: string().optional(),
72790
- config: record(string(), unknown())
72791
- }), object({
72792
- ok: boolean(),
72793
- error: string().optional()
72794
- }), { auth: "admin" });
72934
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
72795
72935
  /** One table of the settings database. */
72796
72936
  var StorageTableSchema = object({
72797
72937
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -74022,7 +74162,7 @@ var TerminalInstanceInfoSchema = object({
74022
74162
  /**
74023
74163
  * False while the node the instance is bound to is not in the cluster
74024
74164
  * topology (forgotten, renamed, re-paired). The instance is kept; the
74025
- * operator re-points it with `updateInstance({ targetNodeId })`.
74165
+ * operator re-points it on the Terminal page.
74026
74166
  */
74027
74167
  nodeInCluster: boolean(),
74028
74168
  profileId: string(),
@@ -74060,41 +74200,7 @@ var TerminalOutputBatchSchema = object({
74060
74200
  snapshot: string().optional(),
74061
74201
  events: array(TerminalOutputEventSchema).readonly()
74062
74202
  });
74063
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
74064
- targetNodeId: string().min(1),
74065
- profileId: string().min(1),
74066
- name: string().trim().min(1).max(160).optional(),
74067
- executable: string().max(1024).optional(),
74068
- args: array(string().max(2048)).max(64).optional(),
74069
- cwd: string().max(1024).optional(),
74070
- environment: array(string().max(4096)).max(64).optional(),
74071
- profileSettings: ProfileSettingsBagSchema.optional()
74072
- }), TerminalInstanceInfoSchema, {
74073
- kind: "mutation",
74074
- auth: "admin"
74075
- }), method(object({
74076
- instanceId: string().min(1),
74077
- /** Re-point the instance to another node; the profile must exist there. */
74078
- targetNodeId: string().min(1).max(256).optional(),
74079
- name: string().trim().min(1).max(160).optional(),
74080
- executable: string().max(1024).optional(),
74081
- args: array(string().max(2048)).max(64).optional(),
74082
- cwd: string().max(1024).optional(),
74083
- environment: array(string().max(4096)).max(64).optional(),
74084
- profileSettings: ProfileSettingsBagSchema.optional()
74085
- }), TerminalInstanceInfoSchema, {
74086
- kind: "mutation",
74087
- auth: "admin"
74088
- }), method(object({ instanceId: string().min(1) }), _void(), {
74089
- kind: "mutation",
74090
- auth: "admin"
74091
- }), method(object({
74092
- instanceId: string().min(1),
74093
- enabled: boolean()
74094
- }), TerminalInstanceInfoSchema, {
74095
- kind: "mutation",
74096
- auth: "admin"
74097
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74203
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74098
74204
  stableId: string().min(1),
74099
74205
  name: string().trim().min(1).max(160).optional()
74100
74206
  }), TerminalInstanceInfoSchema, {
@@ -74289,7 +74395,7 @@ var TrainingProviderTypeSchema = object({
74289
74395
  label: string(),
74290
74396
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
74291
74397
  iconAsset: string(),
74292
- /** Owning `training-provider` addon — collection selector for createEntry. */
74398
+ /** Owning `training-provider` addon. */
74293
74399
  addonId: string(),
74294
74400
  /**
74295
74401
  * Fields for one entry of this type (credentials + defaults). Absent on a
@@ -74334,36 +74440,6 @@ var TrainingProviderDescriptorSchema = object({
74334
74440
  */
74335
74441
  formValues: record(string(), unknown()).optional()
74336
74442
  });
74337
- /**
74338
- * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
74339
- * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
74340
- * `{ $secretClear: true }` clears it.
74341
- */
74342
- var TrainingProviderUpdateEntryInputSchema = object({
74343
- entryId: string().min(1),
74344
- name: string().min(1),
74345
- tokenId: string().optional(),
74346
- tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
74347
- environment: string().optional(),
74348
- gpuType: string().optional(),
74349
- region: string().optional(),
74350
- costCapUsd: number().nonnegative().optional(),
74351
- maxRuntimeSec: number().int().positive().optional(),
74352
- failedDatasetGraceHours: number().nonnegative().optional()
74353
- });
74354
- var TrainingCredentialCheckSchema = object({
74355
- ok: boolean(),
74356
- /** Workspace / account name, when the provider says. */
74357
- account: string().optional(),
74358
- reason: _enum([
74359
- "missing",
74360
- "invalid",
74361
- "forbidden",
74362
- "network",
74363
- "quota"
74364
- ]).optional(),
74365
- detail: string().optional()
74366
- });
74367
74443
  var ProviderJobStateSchema = _enum([
74368
74444
  "pending",
74369
74445
  "starting",
@@ -74411,22 +74487,6 @@ var TrainingLogLineSchema = object({
74411
74487
  text: string()
74412
74488
  });
74413
74489
  method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
74414
- typeId: string().min(1),
74415
- name: string().min(1)
74416
- }), object({ entryId: string() }), {
74417
- kind: "mutation",
74418
- auth: "admin"
74419
- }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
74420
- kind: "mutation",
74421
- auth: "admin"
74422
- }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
74423
- kind: "mutation",
74424
- auth: "admin"
74425
- }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
74426
- kind: "mutation",
74427
- auth: "admin",
74428
- timeoutMs: 3e4
74429
- }), method(object({
74430
74490
  entryId: string().min(1),
74431
74491
  gpuType: string(),
74432
74492
  region: string(),
@@ -76958,6 +77018,20 @@ Object.freeze({
76958
77018
  access: "view",
76959
77019
  role: "admin"
76960
77020
  },
77021
+ "addonSettings.getScopedSettings": {
77022
+ capName: "addon-settings",
77023
+ capScope: "system",
77024
+ addonId: null,
77025
+ access: "view",
77026
+ role: "admin"
77027
+ },
77028
+ "addonSettings.readScopedValues": {
77029
+ capName: "addon-settings",
77030
+ capScope: "system",
77031
+ addonId: null,
77032
+ access: "view",
77033
+ role: "admin"
77034
+ },
76961
77035
  "addonSettings.unsetStoredSettingsKeys": {
76962
77036
  capName: "addon-settings",
76963
77037
  capScope: "system",
@@ -76979,6 +77053,13 @@ Object.freeze({
76979
77053
  access: "create",
76980
77054
  role: "admin"
76981
77055
  },
77056
+ "addonSettings.updateScopedSettings": {
77057
+ capName: "addon-settings",
77058
+ capScope: "system",
77059
+ addonId: null,
77060
+ access: "create",
77061
+ role: "admin"
77062
+ },
76982
77063
  "addonWidgets.listWidgets": {
76983
77064
  capName: "addon-widgets",
76984
77065
  capScope: "system",
@@ -77812,20 +77893,6 @@ Object.freeze({
77812
77893
  access: "view",
77813
77894
  role: "admin"
77814
77895
  },
77815
- "connectionTest.describeTest": {
77816
- capName: "connection-test",
77817
- capScope: "system",
77818
- addonId: null,
77819
- access: "view",
77820
- role: "admin"
77821
- },
77822
- "connectionTest.testSettings": {
77823
- capName: "connection-test",
77824
- capScope: "system",
77825
- addonId: null,
77826
- access: "create",
77827
- role: "admin"
77828
- },
77829
77896
  "connectivity.getStatus": {
77830
77897
  capName: "connectivity",
77831
77898
  capScope: "device",
@@ -80192,13 +80259,6 @@ Object.freeze({
80192
80259
  access: "view",
80193
80260
  role: "admin"
80194
80261
  },
80195
- "integrations.testConnection": {
80196
- capName: "integrations",
80197
- capScope: "system",
80198
- addonId: null,
80199
- access: "create",
80200
- role: "admin"
80201
- },
80202
80262
  "integrations.update": {
80203
80263
  capName: "integrations",
80204
80264
  capScope: "system",
@@ -80451,13 +80511,6 @@ Object.freeze({
80451
80511
  access: "delete",
80452
80512
  role: "admin"
80453
80513
  },
80454
- "llm.deleteProfile": {
80455
- capName: "llm",
80456
- capScope: "system",
80457
- addonId: null,
80458
- access: "delete",
80459
- role: "admin"
80460
- },
80461
80514
  "llm.generate": {
80462
80515
  capName: "llm",
80463
80516
  capScope: "system",
@@ -80521,20 +80574,6 @@ Object.freeze({
80521
80574
  access: "view",
80522
80575
  role: "admin"
80523
80576
  },
80524
- "llm.listProfileKinds": {
80525
- capName: "llm",
80526
- capScope: "system",
80527
- addonId: null,
80528
- access: "view",
80529
- role: "admin"
80530
- },
80531
- "llm.listProfiles": {
80532
- capName: "llm",
80533
- capScope: "system",
80534
- addonId: null,
80535
- access: "view",
80536
- role: "admin"
80537
- },
80538
80577
  "llm.listRuntimeNodes": {
80539
80578
  capName: "llm",
80540
80579
  capScope: "system",
@@ -80542,13 +80581,6 @@ Object.freeze({
80542
80581
  access: "view",
80543
80582
  role: "admin"
80544
80583
  },
80545
- "llm.migrateRawSecrets": {
80546
- capName: "llm",
80547
- capScope: "system",
80548
- addonId: null,
80549
- access: "create",
80550
- role: "admin"
80551
- },
80552
80584
  "llm.resolveModelRef": {
80553
80585
  capName: "llm",
80554
80586
  capScope: "system",
@@ -80577,20 +80609,6 @@ Object.freeze({
80577
80609
  access: "create",
80578
80610
  role: "admin"
80579
80611
  },
80580
- "llm.testProfile": {
80581
- capName: "llm",
80582
- capScope: "system",
80583
- addonId: null,
80584
- access: "create",
80585
- role: "admin"
80586
- },
80587
- "llm.upsertProfile": {
80588
- capName: "llm",
80589
- capScope: "system",
80590
- addonId: null,
80591
- access: "create",
80592
- role: "admin"
80593
- },
80594
80612
  "llmRuntime.appleAvailability": {
80595
80613
  capName: "llm-runtime",
80596
80614
  capScope: "system",
@@ -81095,13 +81113,6 @@ Object.freeze({
81095
81113
  access: "create",
81096
81114
  role: "admin"
81097
81115
  },
81098
- "meshNetwork.testConnection": {
81099
- capName: "mesh-network",
81100
- capScope: "system",
81101
- addonId: null,
81102
- access: "create",
81103
- role: "admin"
81104
- },
81105
81116
  "meter.getStatus": {
81106
81117
  capName: "meter",
81107
81118
  capScope: "device",
@@ -81431,13 +81442,6 @@ Object.freeze({
81431
81442
  access: "create",
81432
81443
  role: "admin"
81433
81444
  },
81434
- "mqttBroker.testConnection": {
81435
- capName: "mqtt-broker",
81436
- capScope: "system",
81437
- addonId: null,
81438
- access: "create",
81439
- role: "admin"
81440
- },
81441
81445
  "nativeObjectDetection.getOptions": {
81442
81446
  capName: "native-object-detection",
81443
81447
  capScope: "device",
@@ -81767,13 +81771,6 @@ Object.freeze({
81767
81771
  access: "create",
81768
81772
  role: "admin"
81769
81773
  },
81770
- "notificationOutput.deleteTarget": {
81771
- capName: "notification-output",
81772
- capScope: "system",
81773
- addonId: null,
81774
- access: "delete",
81775
- role: "admin"
81776
- },
81777
81774
  "notificationOutput.discoverTargets": {
81778
81775
  capName: "notification-output",
81779
81776
  capScope: "system",
@@ -81781,13 +81778,6 @@ Object.freeze({
81781
81778
  access: "view",
81782
81779
  role: "admin"
81783
81780
  },
81784
- "notificationOutput.getTarget": {
81785
- capName: "notification-output",
81786
- capScope: "system",
81787
- addonId: null,
81788
- access: "view",
81789
- role: "admin"
81790
- },
81791
81781
  "notificationOutput.listTargetKinds": {
81792
81782
  capName: "notification-output",
81793
81783
  capScope: "system",
@@ -81795,13 +81785,6 @@ Object.freeze({
81795
81785
  access: "view",
81796
81786
  role: "admin"
81797
81787
  },
81798
- "notificationOutput.listTargets": {
81799
- capName: "notification-output",
81800
- capScope: "system",
81801
- addonId: null,
81802
- access: "view",
81803
- role: "admin"
81804
- },
81805
81788
  "notificationOutput.probeTargetConfig": {
81806
81789
  capName: "notification-output",
81807
81790
  capScope: "system",
@@ -81816,13 +81799,6 @@ Object.freeze({
81816
81799
  access: "create",
81817
81800
  role: "admin"
81818
81801
  },
81819
- "notificationOutput.setTargetEnabled": {
81820
- capName: "notification-output",
81821
- capScope: "system",
81822
- addonId: null,
81823
- access: "create",
81824
- role: "admin"
81825
- },
81826
81802
  "notificationOutput.testTarget": {
81827
81803
  capName: "notification-output",
81828
81804
  capScope: "system",
@@ -81830,13 +81806,6 @@ Object.freeze({
81830
81806
  access: "create",
81831
81807
  role: "admin"
81832
81808
  },
81833
- "notificationOutput.upsertTarget": {
81834
- capName: "notification-output",
81835
- capScope: "system",
81836
- addonId: null,
81837
- access: "create",
81838
- role: "admin"
81839
- },
81840
81809
  "notificationRules.activateAlarmProfile": {
81841
81810
  capName: "notification-rules",
81842
81811
  capScope: "system",
@@ -82096,41 +82065,6 @@ Object.freeze({
82096
82065
  access: "view",
82097
82066
  role: "admin"
82098
82067
  },
82099
- "oidcConnections.connectionFormSchema": {
82100
- capName: "oidc-connections",
82101
- capScope: "system",
82102
- addonId: null,
82103
- access: "view",
82104
- role: "admin"
82105
- },
82106
- "oidcConnections.createConnection": {
82107
- capName: "oidc-connections",
82108
- capScope: "system",
82109
- addonId: null,
82110
- access: "create",
82111
- role: "admin"
82112
- },
82113
- "oidcConnections.deleteConnection": {
82114
- capName: "oidc-connections",
82115
- capScope: "system",
82116
- addonId: null,
82117
- access: "delete",
82118
- role: "admin"
82119
- },
82120
- "oidcConnections.listConnections": {
82121
- capName: "oidc-connections",
82122
- capScope: "system",
82123
- addonId: null,
82124
- access: "view",
82125
- role: "admin"
82126
- },
82127
- "oidcConnections.updateConnection": {
82128
- capName: "oidc-connections",
82129
- capScope: "system",
82130
- addonId: null,
82131
- access: "create",
82132
- role: "admin"
82133
- },
82134
82068
  "osd.getStatus": {
82135
82069
  capName: "osd",
82136
82070
  capScope: "device",
@@ -85162,13 +85096,6 @@ Object.freeze({
85162
85096
  access: "delete",
85163
85097
  role: "admin"
85164
85098
  },
85165
- "storage.deleteLocation": {
85166
- capName: "storage",
85167
- capScope: "system",
85168
- addonId: null,
85169
- access: "delete",
85170
- role: "admin"
85171
- },
85172
85099
  "storage.endDownload": {
85173
85100
  capName: "storage",
85174
85101
  capScope: "system",
@@ -85225,13 +85152,6 @@ Object.freeze({
85225
85152
  access: "view",
85226
85153
  role: "admin"
85227
85154
  },
85228
- "storage.listProviders": {
85229
- capName: "storage",
85230
- capScope: "system",
85231
- addonId: null,
85232
- access: "view",
85233
- role: "admin"
85234
- },
85235
85155
  "storage.read": {
85236
85156
  capName: "storage",
85237
85157
  capScope: "system",
@@ -85253,27 +85173,6 @@ Object.freeze({
85253
85173
  access: "view",
85254
85174
  role: "admin"
85255
85175
  },
85256
- "storage.testConfig": {
85257
- capName: "storage",
85258
- capScope: "system",
85259
- addonId: null,
85260
- access: "view",
85261
- role: "admin"
85262
- },
85263
- "storage.testLocation": {
85264
- capName: "storage",
85265
- capScope: "system",
85266
- addonId: null,
85267
- access: "view",
85268
- role: "admin"
85269
- },
85270
- "storage.upsertLocation": {
85271
- capName: "storage",
85272
- capScope: "system",
85273
- addonId: null,
85274
- access: "create",
85275
- role: "admin"
85276
- },
85277
85176
  "storage.write": {
85278
85177
  capName: "storage",
85279
85178
  capScope: "system",
@@ -86051,27 +85950,6 @@ Object.freeze({
86051
85950
  access: "create",
86052
85951
  role: "admin"
86053
85952
  },
86054
- "terminalSession.createInstance": {
86055
- capName: "terminal-session",
86056
- capScope: "system",
86057
- addonId: null,
86058
- access: "create",
86059
- role: "admin"
86060
- },
86061
- "terminalSession.deleteInstance": {
86062
- capName: "terminal-session",
86063
- capScope: "system",
86064
- addonId: null,
86065
- access: "delete",
86066
- role: "admin"
86067
- },
86068
- "terminalSession.listInstances": {
86069
- capName: "terminal-session",
86070
- capScope: "system",
86071
- addonId: null,
86072
- access: "view",
86073
- role: "admin"
86074
- },
86075
85953
  "terminalSession.listLegacyCameras": {
86076
85954
  capName: "terminal-session",
86077
85955
  capScope: "system",
@@ -86114,20 +85992,6 @@ Object.freeze({
86114
85992
  access: "create",
86115
85993
  role: "admin"
86116
85994
  },
86117
- "terminalSession.setInstanceEnabled": {
86118
- capName: "terminal-session",
86119
- capScope: "system",
86120
- addonId: null,
86121
- access: "create",
86122
- role: "admin"
86123
- },
86124
- "terminalSession.updateInstance": {
86125
- capName: "terminal-session",
86126
- capScope: "system",
86127
- addonId: null,
86128
- access: "create",
86129
- role: "admin"
86130
- },
86131
85995
  "terminalSession.writeInput": {
86132
85996
  capName: "terminal-session",
86133
85997
  capScope: "system",
@@ -86268,20 +86132,6 @@ Object.freeze({
86268
86132
  access: "create",
86269
86133
  role: "admin"
86270
86134
  },
86271
- "trainingProvider.createEntry": {
86272
- capName: "training-provider",
86273
- capScope: "system",
86274
- addonId: null,
86275
- access: "create",
86276
- role: "admin"
86277
- },
86278
- "trainingProvider.deleteEntry": {
86279
- capName: "training-provider",
86280
- capScope: "system",
86281
- addonId: null,
86282
- access: "delete",
86283
- role: "admin"
86284
- },
86285
86135
  "trainingProvider.describe": {
86286
86136
  capName: "training-provider",
86287
86137
  capScope: "system",
@@ -86352,13 +86202,6 @@ Object.freeze({
86352
86202
  access: "create",
86353
86203
  role: "admin"
86354
86204
  },
86355
- "trainingProvider.updateEntry": {
86356
- capName: "training-provider",
86357
- capScope: "system",
86358
- addonId: null,
86359
- access: "create",
86360
- role: "admin"
86361
- },
86362
86205
  "trainingProvider.uploadDataset": {
86363
86206
  capName: "training-provider",
86364
86207
  capScope: "system",
@@ -86366,13 +86209,6 @@ Object.freeze({
86366
86209
  access: "create",
86367
86210
  role: "admin"
86368
86211
  },
86369
- "trainingProvider.validateCredentials": {
86370
- capName: "training-provider",
86371
- capScope: "system",
86372
- addonId: null,
86373
- access: "create",
86374
- role: "admin"
86375
- },
86376
86212
  "turnProvider.getTurnServers": {
86377
86213
  capName: "turn-provider",
86378
86214
  capScope: "system",
@@ -146087,17 +145923,6 @@ function scrub(message, secret) {
146087
145923
  function mintRelayId() {
146088
145924
  return `relay-${(0, node_crypto.randomUUID)().slice(0, 8)}`;
146089
145925
  }
146090
- /** The keys the settings page edits: the relay's connection fields. */
146091
- var RELAY_FORM_KEYS = new Set([
146092
- "host",
146093
- "port",
146094
- "secure",
146095
- "username",
146096
- "password",
146097
- "fromAddress",
146098
- "fromName"
146099
- ]);
146100
- var DEFAULT_RELAY_NAME = "Email (SMTP)";
146101
145926
  var RELAY_LIST_MAX = 200;
146102
145927
  function hasText(value) {
146103
145928
  return typeof value === "string" && value.length > 0;
@@ -146350,44 +146175,10 @@ var SmtpNodemailerAddon = class extends BaseAddon {
146350
146175
  const rows = (await records.kind(smtpRelaysKind).query({ limit: RELAY_LIST_MAX })).records;
146351
146176
  return (rows.find((row) => row.data.host.length > 0) ?? rows[0])?.id;
146352
146177
  }
146353
- /** The relay form. Its values live on the relay record, never in `addon-settings`. */
146178
+ /** The relay form. */
146354
146179
  async integrationFormSchema(_instance) {
146355
146180
  return this.relayForm();
146356
146181
  }
146357
- async getGlobalSettings() {
146358
- const schema = this.relayForm();
146359
- if (this.instances === null) return hydrateSchema(schema, {}, { inlineSecretsAsViews: true });
146360
- const id = await this.firstRelayId();
146361
- return hydrateSchema(schema, (id === void 0 ? null : await this.instances.readResolved(id))?.body ?? {}, { inlineSecretsAsViews: true });
146362
- }
146363
- async updateGlobalSettingsAsOperator(patch) {
146364
- await this.writeDefaultRelay(patch);
146365
- }
146366
- async updateGlobalSettings(patch) {
146367
- await this.writeDefaultRelay(patch);
146368
- }
146369
- /** Write the form's values onto the default relay, creating it on the first save. */
146370
- async writeDefaultRelay(patch) {
146371
- if (this.instances === null) throw new Error("smtp-nodemailer is not initialised");
146372
- const values = Object.fromEntries(Object.entries(patch).filter(([key]) => RELAY_FORM_KEYS.has(key)));
146373
- const existing = await this.firstRelayId();
146374
- if (existing !== void 0) {
146375
- await this.instances.updateInstance({
146376
- id: existing,
146377
- values
146378
- });
146379
- return;
146380
- }
146381
- const id = mintRelayId();
146382
- await this.instances.createInstance({
146383
- id,
146384
- name: DEFAULT_RELAY_NAME
146385
- });
146386
- await this.instances.updateInstance({
146387
- id,
146388
- values
146389
- });
146390
- }
146391
146182
  /** Probe a relay (EHLO + AUTH) with the form's values, falling back to the stored ones. */
146392
146183
  async probe(id, values) {
146393
146184
  const stored = id !== null && this.instances !== null ? await this.instances.getResolved(id) : null;