@camstack/addon-google-services 0.2.42 → 0.2.44

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.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  let node_crypto = require("node:crypto");
6
- //#region ../types/dist/event-category-BmPm98n0.mjs
6
+ //#region ../types/dist/event-category-78tcBemo.mjs
7
7
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
8
8
  EventCategory["SystemBoot"] = "system.boot";
9
9
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -124,6 +124,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
124
124
  */
125
125
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
126
126
  /**
127
+ * An addon is RUNNING although its installed package is below what the
128
+ * running server requires (`camstack.requires.addons` of the server, or the
129
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
130
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
131
+ * operator alert per node and package. The addon is never held back for it.
132
+ *
133
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
134
+ */
135
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
136
+ /**
137
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
138
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
139
+ *
140
+ * Payload: `{ packageName, addonId, nodeId }`.
141
+ */
142
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
143
+ /**
127
144
  * Monitor scheduled the next retry for a failed addon. Transient —
128
145
  * surfaced to the UI for live-updating the "next retry in Ns"
129
146
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -234,7 +251,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
234
251
  EventCategory["ProcessRestarted"] = "process.restarted";
235
252
  /**
236
253
  * The SET of storage locations changed — one was created, edited, enabled,
237
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
254
+ * disabled or deleted through the location instances or the forced-delete action.
238
255
  *
239
256
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
240
257
  * it must also converge on its own periodic path, because a dropped event
@@ -789,7 +806,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
789
806
  return EventCategory;
790
807
  }({});
791
808
  //#endregion
792
- //#region ../types/dist/device-type-D1fH5kaC.mjs
809
+ //#region ../types/dist/device-type-DxCT0krO.mjs
793
810
  /**
794
811
  import { errMsg } from '@camstack/types'
795
812
  * Extract a human-readable message from an unknown error value.
@@ -951,6 +968,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
951
968
  * filters that want "all devices with on-motion behaviour".
952
969
  */
953
970
  DeviceFeature["MotionTrigger"] = "motion-trigger";
971
+ /**
972
+ * Device can be run for a chosen time and then stopped. Mirrors the
973
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
974
+ * a driver, so the flag is derived from the device's bindings
975
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
976
+ */
977
+ DeviceFeature["TimedRun"] = "timed-run";
954
978
  /** Light supports rgb-triplet color via `color` cap. */
955
979
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
956
980
  /** Light supports HSV color via `color` cap. */
@@ -5985,7 +6009,7 @@ var ZodIssueCode = {
5985
6009
  var ZodFirstPartyTypeKind;
5986
6010
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5987
6011
  //#endregion
5988
- //#region ../types/dist/sleep-D7ZKm4nb.mjs
6012
+ //#region ../types/dist/sleep-CGPZyzB5.mjs
5989
6013
  /**
5990
6014
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5991
6015
  * window to float samples (D455).
@@ -6349,6 +6373,59 @@ Object.fromEntries([
6349
6373
  function isValuelessField(field) {
6350
6374
  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";
6351
6375
  }
6376
+ /**
6377
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6378
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6379
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6380
+ * pre-resolving) lets callers both read the effective value AND detect
6381
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6382
+ * malformed input yields an empty map, never throws.
6383
+ */
6384
+ function collectHydratedFieldEntries(schema) {
6385
+ const out = /* @__PURE__ */ new Map();
6386
+ if (typeof schema !== "object" || schema === null) return out;
6387
+ const sections = schema.sections;
6388
+ if (!Array.isArray(sections)) return out;
6389
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6390
+ return out;
6391
+ }
6392
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6393
+ function collectFieldEntriesInto(fields, into) {
6394
+ if (!Array.isArray(fields)) return;
6395
+ for (const field of fields) {
6396
+ if (typeof field !== "object" || field === null) continue;
6397
+ const type = field.type;
6398
+ if (type === "group") {
6399
+ collectFieldEntriesInto(field.fields, into);
6400
+ continue;
6401
+ }
6402
+ if (type === "sub-tabs") {
6403
+ const tabs = field.tabs;
6404
+ if (Array.isArray(tabs)) {
6405
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6406
+ }
6407
+ continue;
6408
+ }
6409
+ const key = field.key;
6410
+ if (typeof key !== "string" || key.length === 0) continue;
6411
+ if (!("value" in field)) continue;
6412
+ into.set(key, {
6413
+ value: field.value,
6414
+ default: field.default
6415
+ });
6416
+ }
6417
+ }
6418
+ /**
6419
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6420
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6421
+ * common "just give me the resolved values" case. Built on
6422
+ * {@link collectHydratedFieldEntries}.
6423
+ */
6424
+ function collectHydratedFieldValues(schema) {
6425
+ const out = /* @__PURE__ */ new Map();
6426
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6427
+ return out;
6428
+ }
6352
6429
  function hydrateSchema(schema, values, options = {}) {
6353
6430
  return {
6354
6431
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -7149,6 +7226,7 @@ var RecordIntegrationInstances = class {
7149
7226
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
7150
7227
  this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
7151
7228
  this.nameField = kind.declaration.entry?.nameField ?? "name";
7229
+ if (hooks.kindOf === void 0 && hooks.description.kinds.length !== 1) throw new Error(`record kind '${kind.name}': describe().kinds has ${hooks.description.kinds.length} entries, so kindOf is required to say which one an instance is`);
7152
7230
  }
7153
7231
  /**
7154
7232
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -7190,10 +7268,21 @@ var RecordIntegrationInstances = class {
7190
7268
  }
7191
7269
  return out;
7192
7270
  }
7271
+ /** The kind an instance body is of: the provider's `kindOf`, else its one declared kind. */
7272
+ kindOfBody(body) {
7273
+ if (this.hooks.kindOf !== void 0) {
7274
+ const id = this.hooks.kindOf(body);
7275
+ if (!this.hooks.description.kinds.some((declared) => declared.id === id)) throw new Error(`record kind '${this.kind.name}': kindOf returned '${id}', which describe().kinds does not declare (D1013)`);
7276
+ return id;
7277
+ }
7278
+ const only = this.hooks.description.kinds[0];
7279
+ if (only === void 0) throw new Error(`record kind '${this.kind.name}' declares no kinds`);
7280
+ return only.id;
7281
+ }
7193
7282
  viewOf(record) {
7194
7283
  const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
7195
7284
  const rawName = record.data[this.nameField];
7196
- const kind = this.hooks.kindOf?.(record.data);
7285
+ const kind = this.kindOfBody(record.data);
7197
7286
  const health = this.hooks.health?.(record.data, record.id);
7198
7287
  return {
7199
7288
  id: record.id,
@@ -7201,7 +7290,7 @@ var RecordIntegrationInstances = class {
7201
7290
  configured: this.hooks.configured(record.data),
7202
7291
  enabled: isInstanceEnabled(record.data),
7203
7292
  formValues: shown,
7204
- ...kind !== void 0 ? { kind } : {},
7293
+ kind,
7205
7294
  ...health?.status !== void 0 ? { status: health.status } : {},
7206
7295
  ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
7207
7296
  };
@@ -7234,17 +7323,35 @@ var RecordIntegrationInstances = class {
7234
7323
  ...kind !== void 0 ? { kind } : {}
7235
7324
  });
7236
7325
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7326
+ const initial = this.hooks.initialBody(name, kind);
7327
+ const patch = this.patchOf(values ?? {});
7328
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7329
+ kind,
7330
+ body: {
7331
+ ...initial,
7332
+ ...patch
7333
+ }
7334
+ }));
7237
7335
  await this.handle.put({
7238
7336
  id: minted,
7239
- data: this.hooks.initialBody(name, kind)
7337
+ data: initial
7240
7338
  }, { ifRev: 0 });
7241
- const patch = this.patchOf(values ?? {});
7242
7339
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7243
7340
  await this.hooks.onChanged?.(minted, "created");
7244
7341
  return { id: minted };
7245
7342
  };
7246
7343
  updateInstance = async ({ id, name, values }) => {
7247
7344
  const patch = this.patchOf(values);
7345
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7346
+ const stored = await this.handle.get(id);
7347
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7348
+ kind: void 0,
7349
+ body: {
7350
+ ...stored.data,
7351
+ ...patch
7352
+ }
7353
+ }));
7354
+ }
7248
7355
  if (name !== void 0) patch[this.nameField] = name;
7249
7356
  await this.handle.patch(id, patch);
7250
7357
  await this.hooks.onChanged?.(id, "updated");
@@ -7555,6 +7662,21 @@ var SCOPED_SETTINGS_SCOPES = [
7555
7662
  "device-defaults",
7556
7663
  "node"
7557
7664
  ];
7665
+ /** The one refusal of a key a scope did not declare. */
7666
+ function assertDeclaredKeys(keys, declared) {
7667
+ const undeclared = keys.filter((key) => !declared.has(key));
7668
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7669
+ }
7670
+ /** Build the handle over an addon's scoped read, write and unset. */
7671
+ function createScopedSettings(deps) {
7672
+ return {
7673
+ async get() {
7674
+ return deps.schema.parse(await deps.read());
7675
+ },
7676
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7677
+ unset: (keys) => deps.unset(keys)
7678
+ };
7679
+ }
7558
7680
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7559
7681
  var KeychainWriteRefusedError = class extends Error {
7560
7682
  fieldPath;
@@ -8130,7 +8252,16 @@ function createHandle(raw, opts) {
8130
8252
  return blob;
8131
8253
  }
8132
8254
  },
8133
- writeAddonStore: writeAddon
8255
+ writeAddonStore: writeAddon,
8256
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8257
+ const stored = await readStoredAddonBlob(raw);
8258
+ index.seen("addon", stored);
8259
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8260
+ if (removable.length === 0) return;
8261
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8262
+ await raw.writeAddonStore(patch);
8263
+ index.wrote("addon", patch);
8264
+ })
8134
8265
  };
8135
8266
  }
8136
8267
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8260,7 +8391,7 @@ function boundOf(list) {
8260
8391
  * Base class for CamStack addons. Eliminates settings boilerplate:
8261
8392
  *
8262
8393
  * - Typed `config` property with automatic resolution from store + defaults
8263
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8394
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8264
8395
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8265
8396
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8266
8397
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8270,7 +8401,7 @@ function boundOf(list) {
8270
8401
  * Subclasses override:
8271
8402
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8272
8403
  * - `onShutdown()` — cleanup
8273
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8404
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8274
8405
  *
8275
8406
  * @example
8276
8407
  * ```ts
@@ -8284,7 +8415,7 @@ function boundOf(list) {
8284
8415
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8285
8416
  * }
8286
8417
  *
8287
- * protected globalSettingsSchema() {
8418
+ * protected clusterSettingsSchema() {
8288
8419
  * return this.schema({
8289
8420
  * sections: [{
8290
8421
  * id: 'main', title: 'Settings',
@@ -8327,7 +8458,7 @@ var BaseAddon = class {
8327
8458
  defaults;
8328
8459
  /**
8329
8460
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8330
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8461
+ * itself (`state()`, the scoped writers) that no form field declares.
8331
8462
  * Stored in the keychain exactly like a `password` field. Syntax:
8332
8463
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8333
8464
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8337,7 +8468,7 @@ var BaseAddon = class {
8337
8468
  /**
8338
8469
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8339
8470
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8340
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8471
+ * see them. Empty until loaded.
8341
8472
  */
8342
8473
  _scopedByCap = /* @__PURE__ */ new Map();
8343
8474
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8523,7 +8654,7 @@ var BaseAddon = class {
8523
8654
  /** Addon-specific cleanup. Override if needed. */
8524
8655
  async onShutdown() {}
8525
8656
  /**
8526
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8657
+ * Called after config is resolved during a settings write.
8527
8658
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8528
8659
  * Not called during initialize() — use onInitialize() for initial setup.
8529
8660
  */
@@ -8552,82 +8683,12 @@ var BaseAddon = class {
8552
8683
  scopedSettingsCapabilities() {
8553
8684
  return [];
8554
8685
  }
8555
- /** Override to provide global-level settings UI schema. */
8556
- globalSettingsSchema(_cap) {
8557
- return null;
8558
- }
8559
8686
  /** Override to provide device-level settings UI schema. */
8560
8687
  deviceSettingsSchema() {
8561
8688
  return null;
8562
8689
  }
8563
8690
  /**
8564
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8565
- * ARE the configuration of its integration.
8566
- *
8567
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8568
- * operator should find on the addon's integration page (System →
8569
- * Integrations → <name>) rather than only in the cluster-wide list of every
8570
- * addon. Empty (the default) means the addon has no integration-level
8571
- * settings and no such surface is offered — this is opt-in, because whether
8572
- * an addon's configuration IS its integration's configuration depends on the
8573
- * nature of the integration.
8574
- *
8575
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8576
- * the ONE global schema, in the ONE addon store, written by the ONE
8577
- * `updateGlobalSettings` path. There is deliberately no
8578
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8579
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8580
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8581
- *
8582
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8583
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8584
- * removed with the reason recorded at
8585
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8586
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8587
- * marker sprinkled across sections also has to borrow a field that already
8588
- * means something else; borrowing `section.tab` put the literal word
8589
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8590
- * GROUP this visually" and cannot also mean "where this lives" (D269
8591
- * supersedes D268). One declaration, in one place, next to the schema whose
8592
- * ids it names.
8593
- */
8594
- integrationSettingSections() {
8595
- return [];
8596
- }
8597
- async getGlobalSettings(overlay, cap, nodeId) {
8598
- await this.loadScopedSchemas(cap);
8599
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8600
- if (!schema) return { sections: [] };
8601
- const projected = await this.readRawProjectedStore(nodeId, cap);
8602
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8603
- }
8604
- /**
8605
- * The integration-level view of this addon's settings: exactly the sections
8606
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8607
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8608
- *
8609
- * Returns `null` when the addon declared nothing — an addon that opts out has
8610
- * no integration settings surface at all, rather than an empty one that reads
8611
- * as a failed load.
8612
- *
8613
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8614
- * and not in whichever UI happens to render this:
8615
- *
8616
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8617
- * shown here is the same field, with the same bare key, that the addon's
8618
- * own page shows. There is no integration-specific writer — callers save
8619
- * through `updateGlobalSettings` — so a second store key is unreachable,
8620
- * not merely discouraged.
8621
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8622
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8623
- * such a field silently picked would be a wrong answer for the operator
8624
- * who opened the page (D266).
8625
- * 3. **No silent typo.** A declared id that names no section throws. The
8626
- * alternative — skip it — turns a rename into a surface that quietly
8627
- * empties, which looks exactly like an addon with nothing to configure.
8628
- */
8629
- /**
8630
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8691
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8631
8692
  *
8632
8693
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8633
8694
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8675,37 +8736,178 @@ var BaseAddon = class {
8675
8736
  }
8676
8737
  }
8677
8738
  /**
8739
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8740
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8741
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8742
+ */
8743
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8744
+ return null;
8745
+ }
8746
+ /**
8747
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8748
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8749
+ */
8750
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8751
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8752
+ clusterSettings(cap, schema) {
8753
+ return this.scopedHandle("cluster", cap, schema);
8754
+ }
8755
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8756
+ deviceDefaults(cap, schema) {
8757
+ return this.scopedHandle("device-defaults", cap, schema);
8758
+ }
8759
+ /**
8760
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8761
+ * with no row yields the schema defaults — never a bare-key fallback.
8762
+ */
8763
+ nodeSettings(cap, schema, nodeId) {
8764
+ return this.scopedHandle("node", cap, schema, nodeId);
8765
+ }
8766
+ /**
8767
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8768
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8769
+ * central delete. `nodeId` omitted ⇒ the local node.
8770
+ */
8771
+ nodeState(key, schema, fallback, nodeId) {
8772
+ this._declaredStateKeys.add(key);
8773
+ return createDurableState({
8774
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8775
+ schema,
8776
+ fallback,
8777
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8778
+ write: async (patch) => {
8779
+ await this._ctx?.settings?.writeAddonStore(patch);
8780
+ },
8781
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8782
+ addonId: this._ctx?.id,
8783
+ key: k,
8784
+ error: String(e)
8785
+ } })
8786
+ });
8787
+ }
8788
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8789
+ async declaredScopedSchema(scope, cap) {
8790
+ await this.loadScopedSchemas(cap);
8791
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8792
+ }
8793
+ async requireScopedSchema(scope, cap) {
8794
+ const schema = await this.declaredScopedSchema(scope, cap);
8795
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8796
+ return schema;
8797
+ }
8798
+ scopedKeys(schema) {
8799
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8800
+ }
8801
+ /** `store` narrowed to the keys `schema` declares. */
8802
+ narrowToScope(store, schema) {
8803
+ const keys = this.scopedKeys(schema);
8804
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8805
+ }
8806
+ scopedHandle(scope, cap, schema, nodeId) {
8807
+ return createScopedSettings({
8808
+ schema,
8809
+ read: async () => {
8810
+ const declared = await this.requireScopedSchema(scope, cap);
8811
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8812
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8813
+ return {
8814
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8815
+ ...stored
8816
+ };
8817
+ },
8818
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8819
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8820
+ });
8821
+ }
8822
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8823
+ async writeScoped(scope, cap, patch, nodeId) {
8824
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8825
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8826
+ }
8827
+ /**
8828
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8829
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8830
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8831
+ */
8832
+ async unsetScoped(scope, cap, keys, nodeId) {
8833
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8834
+ const target = scope === "node" ? nodeId : void 0;
8835
+ const tree = this.addonSecretTree();
8836
+ const secretKeys = keys.filter((key) => tree.has(key));
8837
+ const plainKeys = keys.filter((key) => !tree.has(key));
8838
+ let changed = false;
8839
+ if (plainKeys.length > 0) {
8840
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8841
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8842
+ }
8843
+ if (secretKeys.length > 0) {
8844
+ await this.clearSecretKeys(secretKeys, target, cap);
8845
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8846
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8847
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8848
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8849
+ changed = true;
8850
+ }
8851
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8852
+ }
8853
+ /**
8854
+ * The hydrated form of one scope of one capability's settings (any
8855
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8856
+ */
8857
+ async getScopedSettings(scope, cap, nodeId) {
8858
+ const schema = await this.declaredScopedSchema(scope, cap);
8859
+ if (schema === null) return null;
8860
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8861
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8862
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8863
+ }
8864
+ /**
8865
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8866
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8867
+ * nothing.
8868
+ */
8869
+ async listScopedSettings() {
8870
+ await this.loadScopedSchemas();
8871
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8872
+ cap,
8873
+ scope: entry.scope,
8874
+ sectionIds: entry.schema.sections.map((section) => section.id)
8875
+ })));
8876
+ }
8877
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8878
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8879
+ this._operatorWriteDepth++;
8880
+ try {
8881
+ await this.writeScoped(scope, cap, patch, nodeId);
8882
+ } finally {
8883
+ this._operatorWriteDepth--;
8884
+ }
8885
+ }
8886
+ /**
8887
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8888
+ * its value. `null` when the capability declares nothing for the scope.
8889
+ */
8890
+ async readScopedValues(scope, cap, nodeId) {
8891
+ const schema = await this.declaredScopedSchema(scope, cap);
8892
+ if (schema === null) return null;
8893
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8894
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8895
+ }
8896
+ /**
8678
8897
  * What the create wizard and the integration edit page render for one instance: the addon's
8679
8898
  * per-instance schema when it declares one, else its global schema (what they always showed).
8680
8899
  */
8681
8900
  async getIntegrationFormSchema(instance, nodeId) {
8682
8901
  const scoped = await this.integrationFormSchema(instance);
8683
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8902
+ if (scoped === null) return this.scopedForm(nodeId);
8684
8903
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8685
8904
  }
8686
- async getIntegrationSettings(nodeId) {
8687
- const declared = this.integrationSettingSections();
8688
- if (declared.length === 0) return null;
8689
- const schema = this.globalSettingsSchema();
8690
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8691
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8692
- const sections = [];
8693
- for (const id of declared) {
8694
- const section = byId.get(id);
8695
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8696
- const fields = dropPerNodeFields(section.fields);
8697
- if (fields.length === 0) continue;
8698
- sections.push({
8699
- ...section,
8700
- fields
8701
- });
8702
- }
8703
- if (sections.length === 0) return null;
8704
- const projected = await this.readRawProjectedStore(nodeId);
8705
- return hydrateSchema({
8706
- ...schema,
8707
- sections
8708
- }, projected, { inlineSecretsAsViews: true });
8905
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8906
+ async scopedForm(nodeId) {
8907
+ await this.loadScopedSchemas();
8908
+ const schema = this.scopedFormSchema();
8909
+ if (!schema) return { sections: [] };
8910
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8709
8911
  }
8710
8912
  /**
8711
8913
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8714,8 +8916,8 @@ var BaseAddon = class {
8714
8916
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8715
8917
  * A no-op passthrough when the schema declares no `perNode` field.
8716
8918
  *
8717
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8718
- * the store for custom option logic (option narrowing, value snapping) to
8919
+ * This is the sanctioned way for an addon that needs
8920
+ * the store for custom logic (option narrowing, value snapping) to
8719
8921
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8720
8922
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8721
8923
  *
@@ -8767,7 +8969,7 @@ var BaseAddon = class {
8767
8969
  }
8768
8970
  /**
8769
8971
  * Whether ONE global key of this addon's store was written, read three-way
8770
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
8972
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8771
8973
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8772
8974
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8773
8975
  * not supported here: its stored name is scoped.
@@ -8801,7 +9003,7 @@ var BaseAddon = class {
8801
9003
  }
8802
9004
  /**
8803
9005
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8804
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9006
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8805
9007
  * the operator sends is stored as sent even when it equals a pre-keychain
8806
9008
  * inline value: the operator re-entering a password is a change, an addon
8807
9009
  * writing back what it read is not (D787).
@@ -8814,9 +9016,6 @@ var BaseAddon = class {
8814
9016
  this._operatorWriteDepth--;
8815
9017
  }
8816
9018
  }
8817
- async updateGlobalSettings(patch, nodeId) {
8818
- await this.applyGlobalPatch(patch, nodeId);
8819
- }
8820
9019
  /**
8821
9020
  * REMOVE stored secrets — their keychain values and their markers (D787).
8822
9021
  *
@@ -8826,6 +9025,9 @@ var BaseAddon = class {
8826
9025
  * a keychain (an older kernel) it writes `''`, as such code always did.
8827
9026
  */
8828
9027
  async clearSecrets(keys, nodeId) {
9028
+ await this.clearSecretKeys(keys, nodeId);
9029
+ }
9030
+ async clearSecretKeys(keys, nodeId, cap) {
8829
9031
  const tree = this.addonSecretTree();
8830
9032
  const keychain = this._ctx?.secrets !== void 0;
8831
9033
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8838,25 +9040,26 @@ var BaseAddon = class {
8838
9040
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8839
9041
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8840
9042
  }
8841
- await this.applyGlobalPatch(patch, nodeId);
9043
+ await this.applyGlobalPatch(patch, nodeId, cap);
8842
9044
  }
8843
- async applyGlobalPatch(barePatch, nodeId) {
8844
- await this.loadScopedSchemas();
8845
- const keys = this.perNodeKeys();
9045
+ /** Resolves `true` when the write changed stored values. */
9046
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9047
+ await this.loadScopedSchemas(cap);
9048
+ const keys = this.perNodeKeys(cap);
8846
9049
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8847
9050
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8848
9051
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8849
9052
  const handle = this._secretSettings;
8850
9053
  if (handle !== null) {
8851
9054
  const { changed } = await handle.writeAddonStore(scoped);
8852
- if (!changed) return;
9055
+ if (!changed) return false;
8853
9056
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8854
- if (target !== localNode) return;
9057
+ if (target !== localNode) return true;
8855
9058
  await this.resolveConfig();
8856
9059
  await this.onConfigChanged();
8857
9060
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8858
- const legacy = this.globalSettingsSchema();
8859
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9061
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9062
+ return true;
8860
9063
  }
8861
9064
  /**
8862
9065
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8893,10 +9096,7 @@ var BaseAddon = class {
8893
9096
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8894
9097
  this._addonSecretTree = null;
8895
9098
  }
8896
- /**
8897
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8898
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8899
- */
9099
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8900
9100
  scopedFormSchema(cap) {
8901
9101
  const byId = /* @__PURE__ */ new Map();
8902
9102
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8908,8 +9108,7 @@ var BaseAddon = class {
8908
9108
  return [...this._scopedByCap.values()].flat();
8909
9109
  }
8910
9110
  /**
8911
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8912
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9111
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8913
9112
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8914
9113
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8915
9114
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8918,8 +9117,7 @@ var BaseAddon = class {
8918
9117
  const cacheKey = cap ?? "";
8919
9118
  const cached = this._nodeKeysByCap.get(cacheKey);
8920
9119
  if (cached) return cached;
8921
- const legacy = this.globalSettingsSchema(cap);
8922
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9120
+ const keys = /* @__PURE__ */ new Set();
8923
9121
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8924
9122
  if (scope !== "node") continue;
8925
9123
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9137,8 +9335,6 @@ var BaseAddon = class {
9137
9335
  }
9138
9336
  declaredKeySet() {
9139
9337
  const keys = new Set(Object.keys(this.defaults));
9140
- const schema = this.globalSettingsSchema();
9141
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9142
9338
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9143
9339
  for (const key of this._declaredStateKeys) keys.add(key);
9144
9340
  for (const path of this.secretStatePaths) {
@@ -9236,7 +9432,7 @@ var BaseAddon = class {
9236
9432
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9237
9433
  addonSecretTree() {
9238
9434
  if (this._addonSecretTree === null) {
9239
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9435
+ const tree = /* @__PURE__ */ new Map();
9240
9436
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9241
9437
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9242
9438
  }
@@ -9448,47 +9644,6 @@ function classifyStoreError(err) {
9448
9644
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9449
9645
  return "permanent";
9450
9646
  }
9451
- /**
9452
- * Collect the keys of every field marked `perNode: true`, recursing into
9453
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9454
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9455
- * don't declare `perNode` and are excluded by the `in` narrowing.
9456
- */
9457
- /**
9458
- * The same fields with every `perNode: true` one removed, recursing into layout
9459
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9460
- * with no child is dropped rather than rendered empty.
9461
- *
9462
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9463
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9464
- */
9465
- function dropPerNodeFields(fields) {
9466
- const kept = [];
9467
- for (const field of fields) {
9468
- if (field.type === "group") {
9469
- const inner = dropPerNodeFields(field.fields);
9470
- if (inner.length > 0) kept.push({
9471
- ...field,
9472
- fields: inner
9473
- });
9474
- continue;
9475
- }
9476
- if (field.type === "sub-tabs") {
9477
- const tabs = field.tabs.map((tab) => ({
9478
- ...tab,
9479
- fields: dropPerNodeFields(tab.fields)
9480
- })).filter((tab) => tab.fields.length > 0);
9481
- if (tabs.length > 0) kept.push({
9482
- ...field,
9483
- tabs
9484
- });
9485
- continue;
9486
- }
9487
- if ("perNode" in field && field.perNode === true) continue;
9488
- kept.push(field);
9489
- }
9490
- return kept;
9491
- }
9492
9647
  /** The keys of the value-holding fields, recursing into layout containers. */
9493
9648
  function collectValueFieldKeys(fields) {
9494
9649
  const collected = [];
@@ -9521,21 +9676,6 @@ function collectFieldKeys(fields) {
9521
9676
  }
9522
9677
  return collected;
9523
9678
  }
9524
- function collectPerNodeFieldKeys(fields) {
9525
- const collected = [];
9526
- for (const field of fields) {
9527
- if (field.type === "group") {
9528
- collected.push(...collectPerNodeFieldKeys(field.fields));
9529
- continue;
9530
- }
9531
- if (field.type === "sub-tabs") {
9532
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9533
- continue;
9534
- }
9535
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9536
- }
9537
- return collected;
9538
- }
9539
9679
  /**
9540
9680
  * Normalize an `ICamstackAddon.initialize()` return value into the
9541
9681
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10586,7 +10726,6 @@ var CAP_NAMES = Object.freeze([
10586
10726
  "notifier",
10587
10727
  "numeric-sensor",
10588
10728
  "oauth-integration",
10589
- "oidc-connections",
10590
10729
  "osd",
10591
10730
  "osd-manager",
10592
10731
  "pet-feeder",
@@ -12293,13 +12432,25 @@ Object.freeze({
12293
12432
  "path": "temperature",
12294
12433
  "valueType": "number"
12295
12434
  }],
12296
- "timed-run": [{
12297
- "path": "activeUntil",
12298
- "valueType": "number"
12299
- }, {
12300
- "path": "requestedBy",
12301
- "valueType": "string"
12302
- }],
12435
+ "timed-run": [
12436
+ {
12437
+ "path": "activeUntil",
12438
+ "valueType": "number"
12439
+ },
12440
+ {
12441
+ "path": "requestedBy",
12442
+ "valueType": "string"
12443
+ },
12444
+ {
12445
+ "path": "quickRun.duration",
12446
+ "valueType": "number"
12447
+ },
12448
+ {
12449
+ "path": "quickRun.unit",
12450
+ "valueType": "enum",
12451
+ "enumValues": ["s", "min"]
12452
+ }
12453
+ ],
12303
12454
  "update": [
12304
12455
  {
12305
12456
  "path": "currentVersion",
@@ -12478,8 +12629,7 @@ Object.freeze({
12478
12629
  "device-provider": { "reloadDevice": 18e4 },
12479
12630
  "llm": {
12480
12631
  "generate": 6e5,
12481
- "generateVision": 6e5,
12482
- "testProfile": 6e5
12632
+ "generateVision": 6e5
12483
12633
  },
12484
12634
  "llm-runtime": {
12485
12635
  "complete": 6e5,
@@ -12538,7 +12688,7 @@ function userError(code, params, englishFallback) {
12538
12688
  return new CamstackError(code, params, englishFallback);
12539
12689
  }
12540
12690
  //#endregion
12541
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12691
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12542
12692
  /**
12543
12693
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12544
12694
  *
@@ -13700,6 +13850,387 @@ var NodeColorIdSchema = _enum([
13700
13850
  "node-h"
13701
13851
  ]);
13702
13852
  //#endregion
13853
+ //#region ../types/dist/convert-BAzH15HU.mjs
13854
+ /**
13855
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13856
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13857
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13858
+ * decimals). K derives from `°C = K − 273.15`.
13859
+ */
13860
+ var UNIT_TABLE = {
13861
+ "°C": {
13862
+ dimension: "temperature",
13863
+ factor: 1,
13864
+ offset: 0
13865
+ },
13866
+ "°F": {
13867
+ dimension: "temperature",
13868
+ factor: 5 / 9,
13869
+ offset: -160 / 9
13870
+ },
13871
+ K: {
13872
+ dimension: "temperature",
13873
+ factor: 1,
13874
+ offset: -273.15
13875
+ },
13876
+ hPa: {
13877
+ dimension: "pressure",
13878
+ factor: 1,
13879
+ offset: 0
13880
+ },
13881
+ kPa: {
13882
+ dimension: "pressure",
13883
+ factor: 10,
13884
+ offset: 0
13885
+ },
13886
+ Pa: {
13887
+ dimension: "pressure",
13888
+ factor: .01,
13889
+ offset: 0
13890
+ },
13891
+ mbar: {
13892
+ dimension: "pressure",
13893
+ factor: 1,
13894
+ offset: 0
13895
+ },
13896
+ bar: {
13897
+ dimension: "pressure",
13898
+ factor: 1e3,
13899
+ offset: 0
13900
+ },
13901
+ inHg: {
13902
+ dimension: "pressure",
13903
+ factor: 33.8639,
13904
+ offset: 0
13905
+ },
13906
+ mmHg: {
13907
+ dimension: "pressure",
13908
+ factor: 1.33322,
13909
+ offset: 0
13910
+ },
13911
+ psi: {
13912
+ dimension: "pressure",
13913
+ factor: 68.9476,
13914
+ offset: 0
13915
+ },
13916
+ "m/s": {
13917
+ dimension: "speed",
13918
+ factor: 1,
13919
+ offset: 0
13920
+ },
13921
+ "km/h": {
13922
+ dimension: "speed",
13923
+ factor: 1 / 3.6,
13924
+ offset: 0
13925
+ },
13926
+ mph: {
13927
+ dimension: "speed",
13928
+ factor: .44704,
13929
+ offset: 0
13930
+ },
13931
+ kn: {
13932
+ dimension: "speed",
13933
+ factor: .514444,
13934
+ offset: 0
13935
+ },
13936
+ "mm/h": {
13937
+ dimension: "precipitation-rate",
13938
+ factor: 1,
13939
+ offset: 0
13940
+ },
13941
+ "in/h": {
13942
+ dimension: "precipitation-rate",
13943
+ factor: 25.4,
13944
+ offset: 0
13945
+ },
13946
+ m: {
13947
+ dimension: "length",
13948
+ factor: 1,
13949
+ offset: 0
13950
+ },
13951
+ mm: {
13952
+ dimension: "length",
13953
+ factor: .001,
13954
+ offset: 0
13955
+ },
13956
+ cm: {
13957
+ dimension: "length",
13958
+ factor: .01,
13959
+ offset: 0
13960
+ },
13961
+ km: {
13962
+ dimension: "length",
13963
+ factor: 1e3,
13964
+ offset: 0
13965
+ },
13966
+ in: {
13967
+ dimension: "length",
13968
+ factor: .0254,
13969
+ offset: 0
13970
+ },
13971
+ ft: {
13972
+ dimension: "length",
13973
+ factor: .3048,
13974
+ offset: 0
13975
+ },
13976
+ mi: {
13977
+ dimension: "length",
13978
+ factor: 1609.344,
13979
+ offset: 0
13980
+ },
13981
+ "m²": {
13982
+ dimension: "area",
13983
+ factor: 1,
13984
+ offset: 0
13985
+ },
13986
+ lx: {
13987
+ dimension: "illuminance",
13988
+ factor: 1,
13989
+ offset: 0
13990
+ },
13991
+ "W/m²": {
13992
+ dimension: "irradiance",
13993
+ factor: 1,
13994
+ offset: 0
13995
+ },
13996
+ W: {
13997
+ dimension: "power",
13998
+ factor: 1,
13999
+ offset: 0
14000
+ },
14001
+ kW: {
14002
+ dimension: "power",
14003
+ factor: 1e3,
14004
+ offset: 0
14005
+ },
14006
+ VA: {
14007
+ dimension: "apparent-power",
14008
+ factor: 1,
14009
+ offset: 0
14010
+ },
14011
+ Wh: {
14012
+ dimension: "energy",
14013
+ factor: 1,
14014
+ offset: 0
14015
+ },
14016
+ kWh: {
14017
+ dimension: "energy",
14018
+ factor: 1e3,
14019
+ offset: 0
14020
+ },
14021
+ MWh: {
14022
+ dimension: "energy",
14023
+ factor: 1e6,
14024
+ offset: 0
14025
+ },
14026
+ V: {
14027
+ dimension: "voltage",
14028
+ factor: 1,
14029
+ offset: 0
14030
+ },
14031
+ mV: {
14032
+ dimension: "voltage",
14033
+ factor: .001,
14034
+ offset: 0
14035
+ },
14036
+ A: {
14037
+ dimension: "current",
14038
+ factor: 1,
14039
+ offset: 0
14040
+ },
14041
+ mA: {
14042
+ dimension: "current",
14043
+ factor: .001,
14044
+ offset: 0
14045
+ },
14046
+ ppm: {
14047
+ dimension: "concentration-volume",
14048
+ factor: 1,
14049
+ offset: 0
14050
+ },
14051
+ ppb: {
14052
+ dimension: "concentration-volume",
14053
+ factor: .001,
14054
+ offset: 0
14055
+ },
14056
+ "µg/m³": {
14057
+ dimension: "concentration-mass",
14058
+ factor: 1,
14059
+ offset: 0
14060
+ },
14061
+ "mg/m³": {
14062
+ dimension: "concentration-mass",
14063
+ factor: 1e3,
14064
+ offset: 0
14065
+ },
14066
+ kg: {
14067
+ dimension: "mass",
14068
+ factor: 1,
14069
+ offset: 0
14070
+ },
14071
+ g: {
14072
+ dimension: "mass",
14073
+ factor: .001,
14074
+ offset: 0
14075
+ },
14076
+ lb: {
14077
+ dimension: "mass",
14078
+ factor: .453592,
14079
+ offset: 0
14080
+ },
14081
+ oz: {
14082
+ dimension: "mass",
14083
+ factor: .0283495,
14084
+ offset: 0
14085
+ },
14086
+ "%": {
14087
+ dimension: "percentage",
14088
+ factor: 1,
14089
+ offset: 0
14090
+ },
14091
+ L: {
14092
+ dimension: "volume",
14093
+ factor: 1,
14094
+ offset: 0
14095
+ },
14096
+ mL: {
14097
+ dimension: "volume",
14098
+ factor: .001,
14099
+ offset: 0
14100
+ },
14101
+ gal: {
14102
+ dimension: "volume",
14103
+ factor: 3.785411784,
14104
+ offset: 0
14105
+ },
14106
+ s: {
14107
+ dimension: "duration",
14108
+ factor: 1,
14109
+ offset: 0
14110
+ },
14111
+ ms: {
14112
+ dimension: "duration",
14113
+ factor: .001,
14114
+ offset: 0
14115
+ },
14116
+ min: {
14117
+ dimension: "duration",
14118
+ factor: 60,
14119
+ offset: 0
14120
+ },
14121
+ h: {
14122
+ dimension: "duration",
14123
+ factor: 3600,
14124
+ offset: 0
14125
+ },
14126
+ d: {
14127
+ dimension: "duration",
14128
+ factor: 86400,
14129
+ offset: 0
14130
+ },
14131
+ B: {
14132
+ dimension: "information",
14133
+ factor: 1,
14134
+ offset: 0
14135
+ },
14136
+ KiB: {
14137
+ dimension: "information",
14138
+ factor: 1024,
14139
+ offset: 0
14140
+ },
14141
+ MiB: {
14142
+ dimension: "information",
14143
+ factor: 1024 ** 2,
14144
+ offset: 0
14145
+ },
14146
+ GiB: {
14147
+ dimension: "information",
14148
+ factor: 1024 ** 3,
14149
+ offset: 0
14150
+ },
14151
+ TiB: {
14152
+ dimension: "information",
14153
+ factor: 1024 ** 4,
14154
+ offset: 0
14155
+ },
14156
+ kB: {
14157
+ dimension: "information",
14158
+ factor: 1e3,
14159
+ offset: 0
14160
+ },
14161
+ MB: {
14162
+ dimension: "information",
14163
+ factor: 1e6,
14164
+ offset: 0
14165
+ },
14166
+ GB: {
14167
+ dimension: "information",
14168
+ factor: 1e9,
14169
+ offset: 0
14170
+ },
14171
+ "°": {
14172
+ dimension: "angle",
14173
+ factor: 1,
14174
+ offset: 0
14175
+ },
14176
+ dBFS: {
14177
+ dimension: "sound-level",
14178
+ factor: 1,
14179
+ offset: 0
14180
+ },
14181
+ dBm: {
14182
+ dimension: "signal-level",
14183
+ factor: 1,
14184
+ offset: 0
14185
+ },
14186
+ mired: {
14187
+ dimension: "colour-temperature",
14188
+ factor: 1,
14189
+ offset: 0
14190
+ },
14191
+ kbps: {
14192
+ dimension: "bitrate",
14193
+ factor: 1,
14194
+ offset: 0
14195
+ },
14196
+ Mbps: {
14197
+ dimension: "bitrate",
14198
+ factor: 1e3,
14199
+ offset: 0
14200
+ },
14201
+ fps: {
14202
+ dimension: "rate",
14203
+ factor: 1,
14204
+ offset: 0
14205
+ }
14206
+ };
14207
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14208
+ function specFor(unit) {
14209
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14210
+ }
14211
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14212
+ function toCanonical(value, spec) {
14213
+ return value * spec.factor + spec.offset;
14214
+ }
14215
+ /** Canonical-unit value → the unit described by `spec`. */
14216
+ function fromCanonical(canonical, spec) {
14217
+ return (canonical - spec.offset) / spec.factor;
14218
+ }
14219
+ /**
14220
+ * Graceful variant for the render path: returns the converted value, or
14221
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14222
+ * Never throws — a render caller falls back to the raw value + source unit.
14223
+ */
14224
+ function tryConvertUnit(value, from, to) {
14225
+ if (from === to) return value;
14226
+ const fromSpec = specFor(from);
14227
+ if (!fromSpec) return void 0;
14228
+ const toSpec = specFor(to);
14229
+ if (!toSpec) return void 0;
14230
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14231
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14232
+ }
14233
+ //#endregion
13703
14234
  //#region ../types/dist/index.mjs
13704
14235
  var SMTP_INBOUND_DEFAULT_PORT = 2525;
13705
14236
  var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
@@ -14104,17 +14635,18 @@ function composeInstanceKinds(parts, base) {
14104
14635
  * message to what the kind supports — callers never special-case a service.
14105
14636
  *
14106
14637
  * DESIGN DECISIONS (locked):
14107
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
14108
- * `setTargetEnabled`), each provider persisting via the `settings-store`
14109
- * cap. Rationale: the admin UI needs one uniform surface across the
14110
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
14111
- * alternative would fork the UI per addon and cannot host the
14112
- * discovery→adopt flow.
14113
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
14114
- * the generated cap-mount auto-`concatCollection`-fans them across every
14115
- * registered provider (notifiers addon + HA addon) so one catalog is
14116
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
14117
- * `addonId` the generated collection router extracts from the call input.
14638
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14639
+ * providers serve their targets as `integration-instances` records, one
14640
+ * record kind per target kind, and the admin lists, adds, edits, switches
14641
+ * and deletes them through that cap. This cap keeps what an instance cannot
14642
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14643
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14644
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14645
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14646
+ * cap-mount auto-`concatCollection`-fans them across every registered
14647
+ * provider so one catalog is routable. `send` / `testTarget` /
14648
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14649
+ * collection router extracts from the call input.
14118
14650
  * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14119
14651
  * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14120
14652
  * `storage` / `storage-provider` / `recording` caps over the same path. No
@@ -14359,7 +14891,7 @@ var TargetKindCapsSchema = object({
14359
14891
  * the union is large and not meant for runtime validation here; the exported
14360
14892
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14361
14893
  */
14362
- var ConfigSchemaPassthrough$4 = unknown();
14894
+ var ConfigSchemaPassthrough$3 = unknown();
14363
14895
  var TargetKindSchema = object({
14364
14896
  kind: string(),
14365
14897
  label: string(),
@@ -14396,7 +14928,7 @@ var TargetKindSchema = object({
14396
14928
  * its raster path, which is the safe default for an unknown image.
14397
14929
  */
14398
14930
  iconMediaType: string().optional(),
14399
- configSchema: ConfigSchemaPassthrough$4,
14931
+ configSchema: ConfigSchemaPassthrough$3,
14400
14932
  /**
14401
14933
  * The config keys that decide WHERE this kind's secrets are sent (server,
14402
14934
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14418,9 +14950,11 @@ var TargetKindSchema = object({
14418
14950
  caps: TargetKindCapsSchema
14419
14951
  });
14420
14952
  /**
14421
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14422
- * (return a presence marker only) when serving `listTargets` — never
14423
- * round-trip a stored secret to the UI.
14953
+ * A target as a provider's own code sees it (the notification-center bridge, the
14954
+ * send path): the flat record body split into the name, the switch and the
14955
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14956
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14957
+ * secret to the UI.
14424
14958
  */
14425
14959
  var TargetSchema = object({
14426
14960
  id: string(),
@@ -14463,13 +14997,13 @@ var ProbeTargetConfigResultSchema = object({
14463
14997
  stateInstance: string().optional(),
14464
14998
  error: string().optional()
14465
14999
  });
14466
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
15000
+ method(object({}), array(TargetKindSchema)), method(object({
14467
15001
  kind: string(),
14468
15002
  config: record(string(), unknown()).optional(),
14469
15003
  /**
14470
15004
  * When editing / duplicating, the blank password field never round-trips
14471
15005
  * the stored token. Pass the source target id so the provider can restore
14472
- * secrets server-side before dialling (same merge as upsert).
15006
+ * secrets server-side before dialling.
14473
15007
  */
14474
15008
  targetId: string().optional()
14475
15009
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14492,27 +15026,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14492
15026
  }), TestResultSchema, {
14493
15027
  kind: "mutation",
14494
15028
  auth: "admin"
14495
- }), method(object({
14496
- target: TargetSchema,
14497
- /**
14498
- * Duplicate flow: create a NEW target id while copying secrets from an
14499
- * existing row. The UI never holds the stored token; blank / redacted
14500
- * password fields are restored from this source (D634 still applies if
14501
- * destinations diverge without a re-typed secret).
14502
- */
14503
- cloneSecretsFrom: string().optional()
14504
- }), TargetSchema, {
14505
- kind: "mutation",
14506
- auth: "admin"
14507
- }), method(object({ targetId: string() }), _void(), {
14508
- kind: "mutation",
14509
- auth: "admin"
14510
- }), method(object({
14511
- targetId: string(),
14512
- enabled: boolean()
14513
- }), _void(), {
14514
- kind: "mutation",
14515
- auth: "admin"
14516
15029
  });
14517
15030
  looseObject({
14518
15031
  name: string().min(1),
@@ -22780,7 +23293,6 @@ var SYSTEM_CAP_VOCABULARY = {
22780
23293
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22781
23294
  "notification-rules": system("notification rules — authored on their own page"),
22782
23295
  "oauth-integration": system(IDENTITY),
22783
- "oidc-connections": system(IDENTITY),
22784
23296
  "osd-manager": system(PIPELINE),
22785
23297
  "pipeline-executor": system(PIPELINE),
22786
23298
  "pipeline-orchestrator": system(PIPELINE),
@@ -42801,6 +43313,7 @@ var ExpressionBindingSourceSchema = union([
42801
43313
  object({
42802
43314
  kind: literal("global"),
42803
43315
  sourceStableId: string(),
43316
+ addonId: string().optional(),
42804
43317
  cap: string(),
42805
43318
  fieldPath: string()
42806
43319
  })
@@ -42867,385 +43380,6 @@ var SourceInfoSchema = object({
42867
43380
  raw: record(string(), unknown()).optional()
42868
43381
  });
42869
43382
  /**
42870
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42871
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42872
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42873
- * decimals). K derives from `°C = K − 273.15`.
42874
- */
42875
- var UNIT_TABLE = {
42876
- "°C": {
42877
- dimension: "temperature",
42878
- factor: 1,
42879
- offset: 0
42880
- },
42881
- "°F": {
42882
- dimension: "temperature",
42883
- factor: 5 / 9,
42884
- offset: -160 / 9
42885
- },
42886
- K: {
42887
- dimension: "temperature",
42888
- factor: 1,
42889
- offset: -273.15
42890
- },
42891
- hPa: {
42892
- dimension: "pressure",
42893
- factor: 1,
42894
- offset: 0
42895
- },
42896
- kPa: {
42897
- dimension: "pressure",
42898
- factor: 10,
42899
- offset: 0
42900
- },
42901
- Pa: {
42902
- dimension: "pressure",
42903
- factor: .01,
42904
- offset: 0
42905
- },
42906
- mbar: {
42907
- dimension: "pressure",
42908
- factor: 1,
42909
- offset: 0
42910
- },
42911
- bar: {
42912
- dimension: "pressure",
42913
- factor: 1e3,
42914
- offset: 0
42915
- },
42916
- inHg: {
42917
- dimension: "pressure",
42918
- factor: 33.8639,
42919
- offset: 0
42920
- },
42921
- mmHg: {
42922
- dimension: "pressure",
42923
- factor: 1.33322,
42924
- offset: 0
42925
- },
42926
- psi: {
42927
- dimension: "pressure",
42928
- factor: 68.9476,
42929
- offset: 0
42930
- },
42931
- "m/s": {
42932
- dimension: "speed",
42933
- factor: 1,
42934
- offset: 0
42935
- },
42936
- "km/h": {
42937
- dimension: "speed",
42938
- factor: 1 / 3.6,
42939
- offset: 0
42940
- },
42941
- mph: {
42942
- dimension: "speed",
42943
- factor: .44704,
42944
- offset: 0
42945
- },
42946
- kn: {
42947
- dimension: "speed",
42948
- factor: .514444,
42949
- offset: 0
42950
- },
42951
- "mm/h": {
42952
- dimension: "precipitation-rate",
42953
- factor: 1,
42954
- offset: 0
42955
- },
42956
- "in/h": {
42957
- dimension: "precipitation-rate",
42958
- factor: 25.4,
42959
- offset: 0
42960
- },
42961
- m: {
42962
- dimension: "length",
42963
- factor: 1,
42964
- offset: 0
42965
- },
42966
- mm: {
42967
- dimension: "length",
42968
- factor: .001,
42969
- offset: 0
42970
- },
42971
- cm: {
42972
- dimension: "length",
42973
- factor: .01,
42974
- offset: 0
42975
- },
42976
- km: {
42977
- dimension: "length",
42978
- factor: 1e3,
42979
- offset: 0
42980
- },
42981
- in: {
42982
- dimension: "length",
42983
- factor: .0254,
42984
- offset: 0
42985
- },
42986
- ft: {
42987
- dimension: "length",
42988
- factor: .3048,
42989
- offset: 0
42990
- },
42991
- mi: {
42992
- dimension: "length",
42993
- factor: 1609.344,
42994
- offset: 0
42995
- },
42996
- "m²": {
42997
- dimension: "area",
42998
- factor: 1,
42999
- offset: 0
43000
- },
43001
- lx: {
43002
- dimension: "illuminance",
43003
- factor: 1,
43004
- offset: 0
43005
- },
43006
- "W/m²": {
43007
- dimension: "irradiance",
43008
- factor: 1,
43009
- offset: 0
43010
- },
43011
- W: {
43012
- dimension: "power",
43013
- factor: 1,
43014
- offset: 0
43015
- },
43016
- kW: {
43017
- dimension: "power",
43018
- factor: 1e3,
43019
- offset: 0
43020
- },
43021
- VA: {
43022
- dimension: "apparent-power",
43023
- factor: 1,
43024
- offset: 0
43025
- },
43026
- Wh: {
43027
- dimension: "energy",
43028
- factor: 1,
43029
- offset: 0
43030
- },
43031
- kWh: {
43032
- dimension: "energy",
43033
- factor: 1e3,
43034
- offset: 0
43035
- },
43036
- MWh: {
43037
- dimension: "energy",
43038
- factor: 1e6,
43039
- offset: 0
43040
- },
43041
- V: {
43042
- dimension: "voltage",
43043
- factor: 1,
43044
- offset: 0
43045
- },
43046
- mV: {
43047
- dimension: "voltage",
43048
- factor: .001,
43049
- offset: 0
43050
- },
43051
- A: {
43052
- dimension: "current",
43053
- factor: 1,
43054
- offset: 0
43055
- },
43056
- mA: {
43057
- dimension: "current",
43058
- factor: .001,
43059
- offset: 0
43060
- },
43061
- ppm: {
43062
- dimension: "concentration-volume",
43063
- factor: 1,
43064
- offset: 0
43065
- },
43066
- ppb: {
43067
- dimension: "concentration-volume",
43068
- factor: .001,
43069
- offset: 0
43070
- },
43071
- "µg/m³": {
43072
- dimension: "concentration-mass",
43073
- factor: 1,
43074
- offset: 0
43075
- },
43076
- "mg/m³": {
43077
- dimension: "concentration-mass",
43078
- factor: 1e3,
43079
- offset: 0
43080
- },
43081
- kg: {
43082
- dimension: "mass",
43083
- factor: 1,
43084
- offset: 0
43085
- },
43086
- g: {
43087
- dimension: "mass",
43088
- factor: .001,
43089
- offset: 0
43090
- },
43091
- lb: {
43092
- dimension: "mass",
43093
- factor: .453592,
43094
- offset: 0
43095
- },
43096
- oz: {
43097
- dimension: "mass",
43098
- factor: .0283495,
43099
- offset: 0
43100
- },
43101
- "%": {
43102
- dimension: "percentage",
43103
- factor: 1,
43104
- offset: 0
43105
- },
43106
- L: {
43107
- dimension: "volume",
43108
- factor: 1,
43109
- offset: 0
43110
- },
43111
- mL: {
43112
- dimension: "volume",
43113
- factor: .001,
43114
- offset: 0
43115
- },
43116
- gal: {
43117
- dimension: "volume",
43118
- factor: 3.785411784,
43119
- offset: 0
43120
- },
43121
- s: {
43122
- dimension: "duration",
43123
- factor: 1,
43124
- offset: 0
43125
- },
43126
- ms: {
43127
- dimension: "duration",
43128
- factor: .001,
43129
- offset: 0
43130
- },
43131
- min: {
43132
- dimension: "duration",
43133
- factor: 60,
43134
- offset: 0
43135
- },
43136
- h: {
43137
- dimension: "duration",
43138
- factor: 3600,
43139
- offset: 0
43140
- },
43141
- d: {
43142
- dimension: "duration",
43143
- factor: 86400,
43144
- offset: 0
43145
- },
43146
- B: {
43147
- dimension: "information",
43148
- factor: 1,
43149
- offset: 0
43150
- },
43151
- KiB: {
43152
- dimension: "information",
43153
- factor: 1024,
43154
- offset: 0
43155
- },
43156
- MiB: {
43157
- dimension: "information",
43158
- factor: 1024 ** 2,
43159
- offset: 0
43160
- },
43161
- GiB: {
43162
- dimension: "information",
43163
- factor: 1024 ** 3,
43164
- offset: 0
43165
- },
43166
- TiB: {
43167
- dimension: "information",
43168
- factor: 1024 ** 4,
43169
- offset: 0
43170
- },
43171
- kB: {
43172
- dimension: "information",
43173
- factor: 1e3,
43174
- offset: 0
43175
- },
43176
- MB: {
43177
- dimension: "information",
43178
- factor: 1e6,
43179
- offset: 0
43180
- },
43181
- GB: {
43182
- dimension: "information",
43183
- factor: 1e9,
43184
- offset: 0
43185
- },
43186
- "°": {
43187
- dimension: "angle",
43188
- factor: 1,
43189
- offset: 0
43190
- },
43191
- dBFS: {
43192
- dimension: "sound-level",
43193
- factor: 1,
43194
- offset: 0
43195
- },
43196
- dBm: {
43197
- dimension: "signal-level",
43198
- factor: 1,
43199
- offset: 0
43200
- },
43201
- mired: {
43202
- dimension: "colour-temperature",
43203
- factor: 1,
43204
- offset: 0
43205
- },
43206
- kbps: {
43207
- dimension: "bitrate",
43208
- factor: 1,
43209
- offset: 0
43210
- },
43211
- Mbps: {
43212
- dimension: "bitrate",
43213
- factor: 1e3,
43214
- offset: 0
43215
- },
43216
- fps: {
43217
- dimension: "rate",
43218
- factor: 1,
43219
- offset: 0
43220
- }
43221
- };
43222
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43223
- function specFor(unit) {
43224
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43225
- }
43226
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43227
- function toCanonical(value, spec) {
43228
- return value * spec.factor + spec.offset;
43229
- }
43230
- /** Canonical-unit value → the unit described by `spec`. */
43231
- function fromCanonical(canonical, spec) {
43232
- return (canonical - spec.offset) / spec.factor;
43233
- }
43234
- /**
43235
- * Graceful variant for the render path: returns the converted value, or
43236
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43237
- * Never throws — a render caller falls back to the raw value + source unit.
43238
- */
43239
- function tryConvertUnit(value, from, to) {
43240
- if (from === to) return value;
43241
- const fromSpec = specFor(from);
43242
- if (!fromSpec) return void 0;
43243
- const toSpec = specFor(to);
43244
- if (!toSpec) return void 0;
43245
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43246
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43247
- }
43248
- /**
43249
43383
  * A number a person types for a physical quantity — the input side of the
43250
43384
  * value + unit contract (D972).
43251
43385
  *
@@ -44941,15 +45075,28 @@ var SettingsSchemaWithValuesSchema = object({
44941
45075
  });
44942
45076
  /** Patch object — keys are field names, values are the new field values. */
44943
45077
  var SettingsPatchSchema = record(string(), unknown());
45078
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45079
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
45080
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
45081
+ var ScopedSettingsDeclarationSchema = object({
45082
+ cap: string(),
45083
+ scope: ScopedSettingsScopeSchema,
45084
+ sectionIds: array(string())
45085
+ });
44944
45086
  /** Standard success response for update operations. */
44945
45087
  var SettingsUpdateResultSchema = object({ success: literal(true) });
44946
45088
  method(object({
44947
45089
  addonId: string(),
44948
- nodeId: string().optional(),
44949
- overlay: record(string(), unknown()).optional(),
44950
- cap: string().optional()
45090
+ cap: string(),
45091
+ scope: ScopedSettingsScopeSchema,
45092
+ nodeId: string().optional()
44951
45093
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44952
45094
  addonId: string(),
45095
+ nodeId: string().optional()
45096
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
45097
+ addonId: string(),
45098
+ cap: string(),
45099
+ scope: ScopedSettingsScopeSchema,
44953
45100
  nodeId: string().optional(),
44954
45101
  patch: SettingsPatchSchema
44955
45102
  }), SettingsUpdateResultSchema, {
@@ -44957,8 +45104,10 @@ method(object({
44957
45104
  auth: "admin"
44958
45105
  }), method(object({
44959
45106
  addonId: string(),
45107
+ cap: string(),
45108
+ scope: ScopedSettingsScopeSchema,
44960
45109
  nodeId: string().optional()
44961
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45110
+ }), record(string(), unknown()).nullable()), method(object({
44962
45111
  addonId: string(),
44963
45112
  nodeId: string().optional(),
44964
45113
  instance: IntegrationInstanceRefSchema
@@ -46298,7 +46447,7 @@ var BackupDestinationInfoSchema = object({
46298
46447
  id: string(),
46299
46448
  /**
46300
46449
  * Manifest id of the owning addon. The Settings button on the
46301
- * destination card opens that addon's `globalSettingsSchema` panel.
46450
+ * destination card opens that addon's scoped settings panel.
46302
46451
  */
46303
46452
  addonId: string(),
46304
46453
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -48907,7 +49056,7 @@ DeviceType.MediaPlayer, method(deviceOnly, _void(), {
48907
49056
  kind: "mutation",
48908
49057
  auth: "protected"
48909
49058
  });
48910
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
49059
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.MediaPlayer;
48911
49060
  var NUMBER = {
48912
49061
  kind: "fixed",
48913
49062
  result: ["number"]
@@ -50778,6 +50927,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
50778
50927
  kind: "mutation",
50779
50928
  auth: "admin"
50780
50929
  });
50930
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
50781
50931
  /**
50782
50932
  * A SITE ZONE — a named part of the property that several locations belong to.
50783
50933
  *
@@ -51171,6 +51321,12 @@ var DeviceInfoSchema = object({
51171
51321
  addonId: string(),
51172
51322
  type: _enum(DeviceType),
51173
51323
  name: string(),
51324
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
51325
+ * entity_id style. Generated once from the name at first registration, unique
51326
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
51327
+ * Optional: a record produced by an older build omits it until the boot
51328
+ * backfill has run. */
51329
+ alias: DeviceAliasSchema.optional(),
51174
51330
  /** Operator-organisational location label. `null` when unset. */
51175
51331
  location: string().nullable(),
51176
51332
  /** Soft-disabled flag. */
@@ -51490,6 +51646,25 @@ method(object({
51490
51646
  }), _void(), {
51491
51647
  kind: "mutation",
51492
51648
  auth: "admin"
51649
+ }), method(object({
51650
+ deviceId: number(),
51651
+ alias: string()
51652
+ }), discriminatedUnion("ok", [object({
51653
+ ok: literal(true),
51654
+ alias: DeviceAliasSchema
51655
+ }), object({
51656
+ ok: literal(false),
51657
+ reason: _enum([
51658
+ "empty",
51659
+ "too-long",
51660
+ "grammar",
51661
+ "reserved",
51662
+ "taken"
51663
+ ]),
51664
+ takenBy: number().optional()
51665
+ })]), {
51666
+ kind: "mutation",
51667
+ auth: "admin"
51493
51668
  }), method(object({
51494
51669
  deviceId: number(),
51495
51670
  location: string().nullable()
@@ -54515,7 +54690,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54515
54690
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
54516
54691
  * clears it.
54517
54692
  */
54518
- var ConfigSchemaPassthrough$3 = custom();
54693
+ var ConfigSchemaPassthrough$2 = custom();
54519
54694
  var IntegrationInstanceIdSchema = string().min(1);
54520
54695
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54521
54696
  /**
@@ -54561,7 +54736,16 @@ var IntegrationInstanceKindSchema = object({
54561
54736
  /** Relative hub path to start an interactive account link of an instance of this kind. */
54562
54737
  connectPath: string().min(1).optional(),
54563
54738
  /** Relative hub path to disconnect that account. */
54564
- disconnectPath: string().min(1).optional()
54739
+ disconnectPath: string().min(1).optional(),
54740
+ /**
54741
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
54742
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
54743
+ * `connection` widgets write connection values, `model` widgets own the model choice.
54744
+ */
54745
+ widgets: array(object({
54746
+ id: string().min(1),
54747
+ placement: _enum(["connection", "model"])
54748
+ })).optional()
54565
54749
  });
54566
54750
  /**
54567
54751
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -54602,10 +54786,11 @@ var IntegrationInstanceDescriptionSchema = object({
54602
54786
  /** True when `testInstance` really probes the remote. */
54603
54787
  canTest: boolean(),
54604
54788
  /**
54605
- * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
54606
- * Add flow asks which first and sends it as `createInstance.kind`.
54789
+ * The kinds an instance can be of (D1013 rule 1), each naming its `functionCap`. One entry:
54790
+ * nothing to choose; more than one: the Add flow asks which first and sends it as
54791
+ * `createInstance.kind`. Never empty: every instance belongs to exactly one kind.
54607
54792
  */
54608
- kinds: array(IntegrationInstanceKindSchema).optional(),
54793
+ kinds: array(IntegrationInstanceKindSchema).min(1),
54609
54794
  /**
54610
54795
  * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
54611
54796
  * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
@@ -54641,8 +54826,8 @@ var IntegrationInstanceSchema = object({
54641
54826
  * the body is the one store, `setInstanceEnabled` the one writer (epic #440, step 3).
54642
54827
  */
54643
54828
  enabled: boolean().optional(),
54644
- /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
54645
- kind: string().optional(),
54829
+ /** The kind of this instance: exactly one of `describe().kinds[].id` (D1013 rule 2). */
54830
+ kind: string().min(1),
54646
54831
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
54647
54832
  formValues: record(string(), unknown()),
54648
54833
  status: IntegrationInstanceStatusSchema.optional(),
@@ -54666,7 +54851,7 @@ var integrationInstancesCapability = {
54666
54851
  id: IntegrationInstanceIdSchema.nullable(),
54667
54852
  /** The kind of the create form (`id: null`) of a provider with several. */
54668
54853
  kind: string().min(1).optional()
54669
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54854
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
54670
54855
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54671
54856
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54672
54857
  /**
@@ -55617,12 +55802,12 @@ var LlmModelInfoSchema = object({
55617
55802
  });
55618
55803
  /**
55619
55804
  * The provider-specific part of a profile, validated by the provider that owns
55620
- * the kind (`upsertProfile` refuses what it does not accept). A field its
55805
+ * the kind (saving an instance refuses what it does not accept). A field its
55621
55806
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
55622
55807
  * keychain, never in the row, and reads back as the redaction marker.
55623
55808
  */
55624
55809
  var LlmProviderConfigSchema = record(string(), unknown());
55625
- var LlmProfileSchema = object({
55810
+ object({
55626
55811
  id: string(),
55627
55812
  name: string(),
55628
55813
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -55716,8 +55901,8 @@ var LlmProviderWidgetSchema = object({
55716
55901
  /** ConfigUISchema tree passed through untyped on the wire (the
55717
55902
  * notification-output `ConfigSchemaPassthrough` precedent at
55718
55903
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
55719
- var ConfigSchemaPassthrough$2 = unknown();
55720
- var LlmProfileKindDescriptorSchema = object({
55904
+ var ConfigSchemaPassthrough$1 = unknown();
55905
+ object({
55721
55906
  kind: LlmProfileKindSchema,
55722
55907
  label: string(),
55723
55908
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -55725,9 +55910,9 @@ var LlmProfileKindDescriptorSchema = object({
55725
55910
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
55726
55911
  addonId: string(),
55727
55912
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
55728
- connectionSchema: ConfigSchemaPassthrough$2,
55913
+ connectionSchema: ConfigSchemaPassthrough$1,
55729
55914
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
55730
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
55915
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
55731
55916
  /** The kind's defaults; a model from `listModels` may override any of them. */
55732
55917
  features: LlmModelFeaturesSchema,
55733
55918
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -55856,20 +56041,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
55856
56041
  }), method(object({
55857
56042
  addonId: string().optional(),
55858
56043
  requestId: string()
55859
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
55860
- kind: "mutation",
55861
- auth: "admin"
55862
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
55863
- kind: "mutation",
55864
- auth: "admin"
55865
- }), method(ProfileRefInputSchema, _void(), {
55866
- kind: "mutation",
55867
- auth: "admin"
55868
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
55869
- kind: "mutation",
55870
- auth: "admin",
55871
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
55872
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
56044
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55873
56045
  kind: "mutation",
55874
56046
  auth: "admin"
55875
56047
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -56864,20 +57036,7 @@ method(_void(), MeshStatusSchema), method(object({
56864
57036
  /** Optional hostname override the host should advertise once joined. */
56865
57037
  hostname: string().optional() }), object({
56866
57038
  /** Authentication URL the operator should open in a browser. */
56867
- 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({
56868
- /** Optional auth key — when provided, probes the key validity
56869
- * against the provider's API. Omit when already joined to
56870
- * just ping the daemon. */
56871
- authKey: string().optional() }), object({
56872
- ok: boolean(),
56873
- /** Provider-side identifier resolved by the probe (tailnet
56874
- * name for Tailscale, network id for ZeroTier, etc.). */
56875
- tenant: string().optional(),
56876
- /** Daemon binary version, when reachable. */
56877
- daemonVersion: string().optional(),
56878
- /** Human-readable error when `ok: false`. */
56879
- error: string().optional()
56880
- }), { kind: "mutation" });
57039
+ 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() }));
56881
57040
  object({
56882
57041
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
56883
57042
  lastFetchedAt: number()
@@ -64238,6 +64397,11 @@ var NcScheduleSchema = object({
64238
64397
  * as a STATE SUMMARY at the end of each window of `schedule`.
64239
64398
  */
64240
64399
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
64400
+ _enum([
64401
+ "activation",
64402
+ "deactivation",
64403
+ "schedule"
64404
+ ]);
64241
64405
  /**
64242
64406
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
64243
64407
  * every section; `section` = one notification per section with something to
@@ -64254,11 +64418,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
64254
64418
  * midnight crossing); the send time is each window's END. Refused on the
64255
64419
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
64256
64420
  */
64257
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
64258
- mode: literal("summary"),
64259
- schedule: NcScheduleSchema,
64260
- split: NcStateSummarySplitSchema.optional()
64261
- })]);
64421
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
64422
+ object({ mode: literal("instant") }),
64423
+ object({
64424
+ mode: literal("summary"),
64425
+ schedule: NcScheduleSchema,
64426
+ split: NcStateSummarySplitSchema.optional()
64427
+ }),
64428
+ object({
64429
+ mode: literal("modes"),
64430
+ /** The bad edge, and every kind that has a single edge. */
64431
+ onActivation: boolean(),
64432
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
64433
+ onDeactivation: boolean(),
64434
+ /** A state summary at the end of each window; absent = no summary. */
64435
+ onSchedule: object({
64436
+ schedule: NcScheduleSchema,
64437
+ grouping: NcStateSummarySplitSchema
64438
+ }).optional()
64439
+ })
64440
+ ]);
64262
64441
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
64263
64442
  var NcPlateMatcherSchema = object({
64264
64443
  /**
@@ -66535,61 +66714,6 @@ var OauthIntegrationDescriptorSchema = object({
66535
66714
  });
66536
66715
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
66537
66716
  /**
66538
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
66539
- *
66540
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
66541
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
66542
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
66543
- * immutable after create.
66544
- */
66545
- var ConfigSchemaPassthrough$1 = unknown();
66546
- var OidcConnectionSchema = object({
66547
- id: string().min(1),
66548
- displayName: string(),
66549
- icon: string(),
66550
- issuerUrl: string(),
66551
- clientId: string(),
66552
- /** Client id + secret present — never the secret value. */
66553
- configured: boolean(),
66554
- /**
66555
- * Values for the connection form schema. `clientSecret` is a keychain
66556
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
66557
- */
66558
- formValues: record(string(), unknown()).optional()
66559
- });
66560
- /**
66561
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
66562
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
66563
- */
66564
- var OidcConnectionUpdateInputSchema = object({
66565
- id: string().min(1),
66566
- displayName: string().min(1),
66567
- icon: string().optional(),
66568
- issuerUrl: string().optional(),
66569
- clientId: string().optional(),
66570
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
66571
- redirectUri: string().optional(),
66572
- scopes: string().optional(),
66573
- usernameClaim: _enum([
66574
- "preferred_username",
66575
- "email",
66576
- "sub"
66577
- ]).optional()
66578
- });
66579
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
66580
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
66581
- displayName: string().min(1)
66582
- }), object({ id: string() }), {
66583
- kind: "mutation",
66584
- auth: "admin"
66585
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
66586
- kind: "mutation",
66587
- auth: "admin"
66588
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
66589
- kind: "mutation",
66590
- auth: "admin"
66591
- });
66592
- /**
66593
66717
  * Generic on-screen-display (video overlay) cap. Each camera exposes
66594
66718
  * a firmware-defined list of overlays (channel name, timestamp,
66595
66719
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -71640,6 +71764,7 @@ var RemoteLayoutComponentSchema = object({
71640
71764
  });
71641
71765
  });
71642
71766
  method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
71767
+ DeviceType.MediaPlayer;
71643
71768
  var RunningStateSourceSchema = _enum(["native", "assigned"]);
71644
71769
  object({
71645
71770
  /** null = unknown — never coerced to false (D393). */
@@ -72775,21 +72900,6 @@ method(object({
72775
72900
  */
72776
72901
  ttlSec: union([number().int().positive(), literal("never")]).optional()
72777
72902
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
72778
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
72779
- providerId: string().min(1),
72780
- displayName: string().min(1),
72781
- configSchema: unknown(),
72782
- nodeLocal: boolean(),
72783
- shouldSaveDiskSpace: literal(true),
72784
- minFreePercent: number().min(0).max(100)
72785
- }), object({
72786
- providerId: string().min(1),
72787
- displayName: string().min(1),
72788
- configSchema: unknown(),
72789
- nodeLocal: boolean(),
72790
- shouldSaveDiskSpace: literal(false),
72791
- minFreePercent: literal(null)
72792
- })]);
72793
72903
  method(object({
72794
72904
  location: StorageLocationRefSchema,
72795
72905
  relativePath: string()
@@ -72851,42 +72961,7 @@ method(object({
72851
72961
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
72852
72962
  kind: "mutation",
72853
72963
  auth: "admin"
72854
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
72855
- createdAt: true,
72856
- updatedAt: true
72857
- }), StorageLocationSchema, {
72858
- kind: "mutation",
72859
- auth: "admin"
72860
- }), method(object({
72861
- id: string(),
72862
- force: boolean().optional()
72863
- }), _void(), {
72864
- kind: "mutation",
72865
- auth: "admin"
72866
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
72867
- ok: boolean(),
72868
- error: string().optional()
72869
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
72870
- providerId: string(),
72871
- /**
72872
- * The location this config is an UNSAVED edit of, when there is one.
72873
- *
72874
- * `listLocations` replaces every declared secret with the redaction
72875
- * sentinel, so the edit modal's form state holds the sentinel for any
72876
- * credential the operator did not retype — and posting that here
72877
- * without a way to resolve it makes the provider try to authenticate
72878
- * as `__camstack_redacted__` and report the operator's own working
72879
- * password as wrong. Given this id, the orchestrator restores each
72880
- * sentinel from the stored config (same rule as `upsertLocation`)
72881
- * before dispatching. Omitted by the "Add location" wizard, where
72882
- * every value was typed just now and nothing is stored yet.
72883
- */
72884
- locationId: string().optional(),
72885
- config: record(string(), unknown())
72886
- }), object({
72887
- ok: boolean(),
72888
- error: string().optional()
72889
- }), { auth: "admin" });
72964
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
72890
72965
  /** One table of the settings database. */
72891
72966
  var StorageTableSchema = object({
72892
72967
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -74117,7 +74192,7 @@ var TerminalInstanceInfoSchema = object({
74117
74192
  /**
74118
74193
  * False while the node the instance is bound to is not in the cluster
74119
74194
  * topology (forgotten, renamed, re-paired). The instance is kept; the
74120
- * operator re-points it with `updateInstance({ targetNodeId })`.
74195
+ * operator re-points it on the Terminal page.
74121
74196
  */
74122
74197
  nodeInCluster: boolean(),
74123
74198
  profileId: string(),
@@ -74155,41 +74230,7 @@ var TerminalOutputBatchSchema = object({
74155
74230
  snapshot: string().optional(),
74156
74231
  events: array(TerminalOutputEventSchema).readonly()
74157
74232
  });
74158
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
74159
- targetNodeId: string().min(1),
74160
- profileId: string().min(1),
74161
- name: string().trim().min(1).max(160).optional(),
74162
- executable: string().max(1024).optional(),
74163
- args: array(string().max(2048)).max(64).optional(),
74164
- cwd: string().max(1024).optional(),
74165
- environment: array(string().max(4096)).max(64).optional(),
74166
- profileSettings: ProfileSettingsBagSchema.optional()
74167
- }), TerminalInstanceInfoSchema, {
74168
- kind: "mutation",
74169
- auth: "admin"
74170
- }), method(object({
74171
- instanceId: string().min(1),
74172
- /** Re-point the instance to another node; the profile must exist there. */
74173
- targetNodeId: string().min(1).max(256).optional(),
74174
- name: string().trim().min(1).max(160).optional(),
74175
- executable: string().max(1024).optional(),
74176
- args: array(string().max(2048)).max(64).optional(),
74177
- cwd: string().max(1024).optional(),
74178
- environment: array(string().max(4096)).max(64).optional(),
74179
- profileSettings: ProfileSettingsBagSchema.optional()
74180
- }), TerminalInstanceInfoSchema, {
74181
- kind: "mutation",
74182
- auth: "admin"
74183
- }), method(object({ instanceId: string().min(1) }), _void(), {
74184
- kind: "mutation",
74185
- auth: "admin"
74186
- }), method(object({
74187
- instanceId: string().min(1),
74188
- enabled: boolean()
74189
- }), TerminalInstanceInfoSchema, {
74190
- kind: "mutation",
74191
- auth: "admin"
74192
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74233
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74193
74234
  stableId: string().min(1),
74194
74235
  name: string().trim().min(1).max(160).optional()
74195
74236
  }), TerminalInstanceInfoSchema, {
@@ -74258,6 +74299,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
74258
74299
  _enum(["switch", "pet-fountain"]);
74259
74300
  DeviceType.Heater, DeviceType.PetFountain;
74260
74301
  DeviceType.Heater, DeviceType.PetFountain;
74302
+ /** One duration, as the one-tap run asks for it. */
74303
+ var TimedRunQuickRunSchema = object({
74304
+ duration: number().positive().finite(),
74305
+ unit: TimedRunUnitSchema
74306
+ });
74261
74307
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
74262
74308
  var TimedRunRefusalSchema = _enum([
74263
74309
  "unsupported-device",
@@ -74278,9 +74324,30 @@ object({
74278
74324
  activeUntil: number().nullable(),
74279
74325
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
74280
74326
  requestedBy: string().nullable(),
74327
+ /**
74328
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
74329
+ * the device is gone or its type has no timed run; a status from before this field
74330
+ * existed has none, and a surface falls back to the type's default.
74331
+ */
74332
+ quickRun: TimedRunQuickRunSchema.nullable(),
74281
74333
  lastFetchedAt: number()
74282
74334
  });
74283
- method(object({
74335
+ /** What the settings form needs: the unit, the bounds and the type's default. */
74336
+ var TimedRunOptionsSchema = object({
74337
+ unit: TimedRunUnitSchema,
74338
+ /** The longest run, in `unit`. */
74339
+ maxDuration: number(),
74340
+ defaultDuration: number()
74341
+ });
74342
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
74343
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
74344
+ method(object({ deviceId: number().int() }), TimedRunOptionsSchema), method(object({
74345
+ deviceId: number().int(),
74346
+ settings: TimedRunSettingsPatchSchema
74347
+ }), _void(), {
74348
+ kind: "mutation",
74349
+ auth: "admin"
74350
+ }), method(object({
74284
74351
  deviceId: number().int(),
74285
74352
  duration: number().positive().finite(),
74286
74353
  unit: TimedRunUnitSchema
@@ -76986,21 +77053,28 @@ Object.freeze({
76986
77053
  access: "view",
76987
77054
  role: "admin"
76988
77055
  },
76989
- "addonSettings.getGlobalSettings": {
77056
+ "addonSettings.getIntegrationFormSchema": {
77057
+ capName: "addon-settings",
77058
+ capScope: "system",
77059
+ addonId: null,
77060
+ access: "view",
77061
+ role: "admin"
77062
+ },
77063
+ "addonSettings.getScopedSettings": {
76990
77064
  capName: "addon-settings",
76991
77065
  capScope: "system",
76992
77066
  addonId: null,
76993
77067
  access: "view",
76994
77068
  role: "admin"
76995
77069
  },
76996
- "addonSettings.getIntegrationFormSchema": {
77070
+ "addonSettings.listScopedSettings": {
76997
77071
  capName: "addon-settings",
76998
77072
  capScope: "system",
76999
77073
  addonId: null,
77000
77074
  access: "view",
77001
77075
  role: "admin"
77002
77076
  },
77003
- "addonSettings.getIntegrationSettings": {
77077
+ "addonSettings.readScopedValues": {
77004
77078
  capName: "addon-settings",
77005
77079
  capScope: "system",
77006
77080
  addonId: null,
@@ -77021,7 +77095,7 @@ Object.freeze({
77021
77095
  access: "create",
77022
77096
  role: "admin"
77023
77097
  },
77024
- "addonSettings.updateGlobalSettings": {
77098
+ "addonSettings.updateScopedSettings": {
77025
77099
  capName: "addon-settings",
77026
77100
  capScope: "system",
77027
77101
  addonId: null,
@@ -79191,6 +79265,13 @@ Object.freeze({
79191
79265
  access: "create",
79192
79266
  role: "admin"
79193
79267
  },
79268
+ "deviceManager.setAlias": {
79269
+ capName: "device-manager",
79270
+ capScope: "system",
79271
+ addonId: null,
79272
+ access: "create",
79273
+ role: "admin"
79274
+ },
79194
79275
  "deviceManager.setChildLayout": {
79195
79276
  capName: "device-manager",
79196
79277
  capScope: "system",
@@ -80479,13 +80560,6 @@ Object.freeze({
80479
80560
  access: "delete",
80480
80561
  role: "admin"
80481
80562
  },
80482
- "llm.deleteProfile": {
80483
- capName: "llm",
80484
- capScope: "system",
80485
- addonId: null,
80486
- access: "delete",
80487
- role: "admin"
80488
- },
80489
80563
  "llm.generate": {
80490
80564
  capName: "llm",
80491
80565
  capScope: "system",
@@ -80549,20 +80623,6 @@ Object.freeze({
80549
80623
  access: "view",
80550
80624
  role: "admin"
80551
80625
  },
80552
- "llm.listProfileKinds": {
80553
- capName: "llm",
80554
- capScope: "system",
80555
- addonId: null,
80556
- access: "view",
80557
- role: "admin"
80558
- },
80559
- "llm.listProfiles": {
80560
- capName: "llm",
80561
- capScope: "system",
80562
- addonId: null,
80563
- access: "view",
80564
- role: "admin"
80565
- },
80566
80626
  "llm.listRuntimeNodes": {
80567
80627
  capName: "llm",
80568
80628
  capScope: "system",
@@ -80570,13 +80630,6 @@ Object.freeze({
80570
80630
  access: "view",
80571
80631
  role: "admin"
80572
80632
  },
80573
- "llm.migrateRawSecrets": {
80574
- capName: "llm",
80575
- capScope: "system",
80576
- addonId: null,
80577
- access: "create",
80578
- role: "admin"
80579
- },
80580
80633
  "llm.resolveModelRef": {
80581
80634
  capName: "llm",
80582
80635
  capScope: "system",
@@ -80605,20 +80658,6 @@ Object.freeze({
80605
80658
  access: "create",
80606
80659
  role: "admin"
80607
80660
  },
80608
- "llm.testProfile": {
80609
- capName: "llm",
80610
- capScope: "system",
80611
- addonId: null,
80612
- access: "create",
80613
- role: "admin"
80614
- },
80615
- "llm.upsertProfile": {
80616
- capName: "llm",
80617
- capScope: "system",
80618
- addonId: null,
80619
- access: "create",
80620
- role: "admin"
80621
- },
80622
80661
  "llmRuntime.appleAvailability": {
80623
80662
  capName: "llm-runtime",
80624
80663
  capScope: "system",
@@ -81123,13 +81162,6 @@ Object.freeze({
81123
81162
  access: "create",
81124
81163
  role: "admin"
81125
81164
  },
81126
- "meshNetwork.testConnection": {
81127
- capName: "mesh-network",
81128
- capScope: "system",
81129
- addonId: null,
81130
- access: "create",
81131
- role: "admin"
81132
- },
81133
81165
  "meter.getStatus": {
81134
81166
  capName: "meter",
81135
81167
  capScope: "device",
@@ -81788,13 +81820,6 @@ Object.freeze({
81788
81820
  access: "create",
81789
81821
  role: "admin"
81790
81822
  },
81791
- "notificationOutput.deleteTarget": {
81792
- capName: "notification-output",
81793
- capScope: "system",
81794
- addonId: null,
81795
- access: "delete",
81796
- role: "admin"
81797
- },
81798
81823
  "notificationOutput.discoverTargets": {
81799
81824
  capName: "notification-output",
81800
81825
  capScope: "system",
@@ -81802,13 +81827,6 @@ Object.freeze({
81802
81827
  access: "view",
81803
81828
  role: "admin"
81804
81829
  },
81805
- "notificationOutput.getTarget": {
81806
- capName: "notification-output",
81807
- capScope: "system",
81808
- addonId: null,
81809
- access: "view",
81810
- role: "admin"
81811
- },
81812
81830
  "notificationOutput.listTargetKinds": {
81813
81831
  capName: "notification-output",
81814
81832
  capScope: "system",
@@ -81816,13 +81834,6 @@ Object.freeze({
81816
81834
  access: "view",
81817
81835
  role: "admin"
81818
81836
  },
81819
- "notificationOutput.listTargets": {
81820
- capName: "notification-output",
81821
- capScope: "system",
81822
- addonId: null,
81823
- access: "view",
81824
- role: "admin"
81825
- },
81826
81837
  "notificationOutput.probeTargetConfig": {
81827
81838
  capName: "notification-output",
81828
81839
  capScope: "system",
@@ -81837,13 +81848,6 @@ Object.freeze({
81837
81848
  access: "create",
81838
81849
  role: "admin"
81839
81850
  },
81840
- "notificationOutput.setTargetEnabled": {
81841
- capName: "notification-output",
81842
- capScope: "system",
81843
- addonId: null,
81844
- access: "create",
81845
- role: "admin"
81846
- },
81847
81851
  "notificationOutput.testTarget": {
81848
81852
  capName: "notification-output",
81849
81853
  capScope: "system",
@@ -81851,13 +81855,6 @@ Object.freeze({
81851
81855
  access: "create",
81852
81856
  role: "admin"
81853
81857
  },
81854
- "notificationOutput.upsertTarget": {
81855
- capName: "notification-output",
81856
- capScope: "system",
81857
- addonId: null,
81858
- access: "create",
81859
- role: "admin"
81860
- },
81861
81858
  "notificationRules.activateAlarmProfile": {
81862
81859
  capName: "notification-rules",
81863
81860
  capScope: "system",
@@ -82117,41 +82114,6 @@ Object.freeze({
82117
82114
  access: "view",
82118
82115
  role: "admin"
82119
82116
  },
82120
- "oidcConnections.connectionFormSchema": {
82121
- capName: "oidc-connections",
82122
- capScope: "system",
82123
- addonId: null,
82124
- access: "view",
82125
- role: "admin"
82126
- },
82127
- "oidcConnections.createConnection": {
82128
- capName: "oidc-connections",
82129
- capScope: "system",
82130
- addonId: null,
82131
- access: "create",
82132
- role: "admin"
82133
- },
82134
- "oidcConnections.deleteConnection": {
82135
- capName: "oidc-connections",
82136
- capScope: "system",
82137
- addonId: null,
82138
- access: "delete",
82139
- role: "admin"
82140
- },
82141
- "oidcConnections.listConnections": {
82142
- capName: "oidc-connections",
82143
- capScope: "system",
82144
- addonId: null,
82145
- access: "view",
82146
- role: "admin"
82147
- },
82148
- "oidcConnections.updateConnection": {
82149
- capName: "oidc-connections",
82150
- capScope: "system",
82151
- addonId: null,
82152
- access: "create",
82153
- role: "admin"
82154
- },
82155
82117
  "osd.getStatus": {
82156
82118
  capName: "osd",
82157
82119
  capScope: "device",
@@ -85183,13 +85145,6 @@ Object.freeze({
85183
85145
  access: "delete",
85184
85146
  role: "admin"
85185
85147
  },
85186
- "storage.deleteLocation": {
85187
- capName: "storage",
85188
- capScope: "system",
85189
- addonId: null,
85190
- access: "delete",
85191
- role: "admin"
85192
- },
85193
85148
  "storage.endDownload": {
85194
85149
  capName: "storage",
85195
85150
  capScope: "system",
@@ -85246,13 +85201,6 @@ Object.freeze({
85246
85201
  access: "view",
85247
85202
  role: "admin"
85248
85203
  },
85249
- "storage.listProviders": {
85250
- capName: "storage",
85251
- capScope: "system",
85252
- addonId: null,
85253
- access: "view",
85254
- role: "admin"
85255
- },
85256
85204
  "storage.read": {
85257
85205
  capName: "storage",
85258
85206
  capScope: "system",
@@ -85274,27 +85222,6 @@ Object.freeze({
85274
85222
  access: "view",
85275
85223
  role: "admin"
85276
85224
  },
85277
- "storage.testConfig": {
85278
- capName: "storage",
85279
- capScope: "system",
85280
- addonId: null,
85281
- access: "view",
85282
- role: "admin"
85283
- },
85284
- "storage.testLocation": {
85285
- capName: "storage",
85286
- capScope: "system",
85287
- addonId: null,
85288
- access: "view",
85289
- role: "admin"
85290
- },
85291
- "storage.upsertLocation": {
85292
- capName: "storage",
85293
- capScope: "system",
85294
- addonId: null,
85295
- access: "create",
85296
- role: "admin"
85297
- },
85298
85225
  "storage.write": {
85299
85226
  capName: "storage",
85300
85227
  capScope: "system",
@@ -86072,27 +85999,6 @@ Object.freeze({
86072
85999
  access: "create",
86073
86000
  role: "admin"
86074
86001
  },
86075
- "terminalSession.createInstance": {
86076
- capName: "terminal-session",
86077
- capScope: "system",
86078
- addonId: null,
86079
- access: "create",
86080
- role: "admin"
86081
- },
86082
- "terminalSession.deleteInstance": {
86083
- capName: "terminal-session",
86084
- capScope: "system",
86085
- addonId: null,
86086
- access: "delete",
86087
- role: "admin"
86088
- },
86089
- "terminalSession.listInstances": {
86090
- capName: "terminal-session",
86091
- capScope: "system",
86092
- addonId: null,
86093
- access: "view",
86094
- role: "admin"
86095
- },
86096
86002
  "terminalSession.listLegacyCameras": {
86097
86003
  capName: "terminal-session",
86098
86004
  capScope: "system",
@@ -86135,20 +86041,6 @@ Object.freeze({
86135
86041
  access: "create",
86136
86042
  role: "admin"
86137
86043
  },
86138
- "terminalSession.setInstanceEnabled": {
86139
- capName: "terminal-session",
86140
- capScope: "system",
86141
- addonId: null,
86142
- access: "create",
86143
- role: "admin"
86144
- },
86145
- "terminalSession.updateInstance": {
86146
- capName: "terminal-session",
86147
- capScope: "system",
86148
- addonId: null,
86149
- access: "create",
86150
- role: "admin"
86151
- },
86152
86044
  "terminalSession.writeInput": {
86153
86045
  capName: "terminal-session",
86154
86046
  capScope: "system",
@@ -86163,6 +86055,13 @@ Object.freeze({
86163
86055
  access: "create",
86164
86056
  role: "viewer"
86165
86057
  },
86058
+ "timedRun.getOptions": {
86059
+ capName: "timed-run",
86060
+ capScope: "device",
86061
+ addonId: null,
86062
+ access: "view",
86063
+ role: "viewer"
86064
+ },
86166
86065
  "timedRun.getStatus": {
86167
86066
  capName: "timed-run",
86168
86067
  capScope: "device",
@@ -86177,6 +86076,13 @@ Object.freeze({
86177
86076
  access: "create",
86178
86077
  role: "viewer"
86179
86078
  },
86079
+ "timedRun.setSettings": {
86080
+ capName: "timed-run",
86081
+ capScope: "device",
86082
+ addonId: null,
86083
+ access: "create",
86084
+ role: "admin"
86085
+ },
86180
86086
  "toast.onToast": {
86181
86087
  capName: "toast",
86182
86088
  capScope: "system",
@@ -87107,6 +87013,7 @@ function offered(received) {
87107
87013
  };
87108
87014
  }
87109
87015
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
87016
+ string().min(1).max(4096);
87110
87017
  Object.freeze({
87111
87018
  "accessories.getStatus": [{
87112
87019
  name: "deviceId",
@@ -87790,6 +87697,11 @@ Object.freeze({
87790
87697
  form: "single",
87791
87698
  optional: false
87792
87699
  }],
87700
+ "deviceManager.setAlias": [{
87701
+ name: "deviceId",
87702
+ form: "single",
87703
+ optional: false
87704
+ }],
87793
87705
  "deviceManager.setChildLayout": [{
87794
87706
  name: "deviceId",
87795
87707
  form: "single",
@@ -89854,6 +89766,11 @@ Object.freeze({
89854
89766
  form: "single",
89855
89767
  optional: false
89856
89768
  }],
89769
+ "timedRun.getOptions": [{
89770
+ name: "deviceId",
89771
+ form: "single",
89772
+ optional: false
89773
+ }],
89857
89774
  "timedRun.getStatus": [{
89858
89775
  name: "deviceId",
89859
89776
  form: "single",
@@ -89864,6 +89781,11 @@ Object.freeze({
89864
89781
  form: "single",
89865
89782
  optional: false
89866
89783
  }],
89784
+ "timedRun.setSettings": [{
89785
+ name: "deviceId",
89786
+ form: "single",
89787
+ optional: false
89788
+ }],
89867
89789
  "update.getStatus": [{
89868
89790
  name: "deviceId",
89869
89791
  form: "single",
@@ -90726,6 +90648,7 @@ var STATE_SUMMARY_VARS = [
90726
90648
  [...RULE_TEXT],
90727
90649
  [...RULE_TEXT],
90728
90650
  [...RULE_TEXT],
90651
+ [...RULE_TEXT],
90729
90652
  [...RULE_BODY_ONLY],
90730
90653
  [...RULE_TEXT],
90731
90654
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
@@ -91847,222 +91770,6 @@ var GoogleBrokerRegistry = class {
91847
91770
  }
91848
91771
  };
91849
91772
  //#endregion
91850
- //#region src/google-settings-copy.ts
91851
- /**
91852
- * Operator-facing setup copy for Connectivity → Calendars / Email.
91853
- * Keep Connect as a same-origin relative link so the admin session cookie
91854
- * rides along (an absolute Public URL from a LAN admin tab drops the cookie).
91855
- * Root-relative hrefs must survive `sanitizeInfoHtml` in ui-library.
91856
- */
91857
- /** Enable APIs library in Google Cloud Console. */
91858
- var GOOGLE_CONSOLE_APIS_URL = "https://console.cloud.google.com/apis/library";
91859
- /** Create / manage OAuth Web clients. */
91860
- var GOOGLE_CONSOLE_CREDENTIALS_URL = "https://console.cloud.google.com/apis/credentials";
91861
- function redirectUriDisplay(publicOrigin) {
91862
- if (publicOrigin === null || publicOrigin.length === 0) return `<public-hub>${OAUTH_CALLBACK_SUFFIX}`;
91863
- return `${publicOrigin.replace(/\/$/, "")}${OAUTH_CALLBACK_SUFFIX}`;
91864
- }
91865
- function consoleLinksHtml$1() {
91866
- return [
91867
- `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
91868
- ` · `,
91869
- `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
91870
- ].join("");
91871
- }
91872
- function introHtml(cap, publicOrigin) {
91873
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
91874
- const console = consoleLinksHtml$1();
91875
- if (cap === "smtp-provider") return [
91876
- "<ol>",
91877
- `<li>Open Google Cloud Console (${console}) and enable the <strong>Gmail API</strong> (and Calendar API if you also use Calendars).</li>`,
91878
- "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
91879
- `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
91880
- "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
91881
- `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
91882
- "<li>Set From address (connected account or a verified send-as). Turn this integration off with the page <strong>Enable</strong> toggle when you do not want Gmail send — Calendar can stay connected.</li>",
91883
- "</ol>"
91884
- ].join("");
91885
- if (cap === "calendar-source") return [
91886
- "<ol>",
91887
- `<li>Open Google Cloud Console (${console}) and enable the <strong>Google Calendar API</strong> (and Gmail API if you also use Email).</li>`,
91888
- "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
91889
- `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
91890
- "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
91891
- `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
91892
- "<li>Optionally restrict which calendars are exposed with the filter below.</li>",
91893
- "</ol>"
91894
- ].join("");
91895
- return [
91896
- "<ol>",
91897
- `<li>Open Google Cloud Console (${console}) and enable <strong>Google Calendar API</strong> and <strong>Gmail API</strong>.</li>`,
91898
- "<li>Create a <strong>Web application</strong> OAuth client under Credentials.</li>",
91899
- `<li>Register redirect URI: <code>${redirect}</code> (Network → Public endpoint).</li>`,
91900
- `<li>Paste Client ID / secret, Save, then <a href="${OAUTH_START_PATH}">Connect Google</a>.</li>`,
91901
- "<li>Calendars and Email share one Google account; use each page’s <strong>Enable</strong> toggle to turn a capability off without revoking OAuth.</li>",
91902
- "</ol>"
91903
- ].join("");
91904
- }
91905
- function connectHtml(cap, publicOrigin) {
91906
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
91907
- if (cap === "smtp-provider") return [
91908
- `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
91909
- "Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
91910
- `Google must list exactly: <code>${redirect}</code>. `,
91911
- "If an older Connect predates Gmail send, revoke CamStack in Google Account → Security and Connect again. ",
91912
- `Console: ${consoleLinksHtml$1()}.`
91913
- ].join("");
91914
- if (cap === "calendar-source") return [
91915
- `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
91916
- "Connect requests <code>calendar.readonly</code> (and Gmail send for the Email page). ",
91917
- `Google must list exactly: <code>${redirect}</code>. `,
91918
- `Console: ${consoleLinksHtml$1()}.`
91919
- ].join("");
91920
- return [
91921
- `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a>. `,
91922
- `Redirect URI registered in Google Cloud must be <code>${redirect}</code>. `,
91923
- `Console: ${consoleLinksHtml$1()}.`
91924
- ].join("");
91925
- }
91926
- function escapeHtml$1(s) {
91927
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
91928
- }
91929
- //#endregion
91930
- //#region src/google-broker-settings-schema.ts
91931
- /**
91932
- * Per-kind broker creation/edit form for kind=`google`.
91933
- * Account linking (Sign in) is driven by the kind's `connectPath`
91934
- * on the instances page — not a hard-coded Connect link in this form.
91935
- */
91936
- function consoleLinksHtml() {
91937
- return [
91938
- `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
91939
- ` · `,
91940
- `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
91941
- ].join("");
91942
- }
91943
- /** Setup steps shown on Add / Edit — includes Audience / Test users. */
91944
- function googleBrokerSetupHtml(publicOrigin) {
91945
- const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
91946
- return [
91947
- "<ol>",
91948
- `<li>Open Google Cloud Console (${consoleLinksHtml()}). Create a <strong>new project</strong> (or pick an existing one) from the project picker at the top.</li>`,
91949
- "<li>In that project, enable <strong>Google Calendar API</strong> and/or <strong>Gmail API</strong> (API library).</li>",
91950
- "<li>Open <strong>Google Auth Platform → Audience</strong> (or APIs &amp; Services → OAuth consent screen → <strong>Audience</strong>). User type <strong>External</strong> + publishing status <strong>Testing</strong> is fine for a personal hub. Under <strong>Test users</strong>, add the Google account(s) that will Sign in — otherwise Google returns <code>403: access_denied</code>. (If you only see Branding / Data Access, open the <strong>Audience</strong> tab in the left sidebar of Google Auth Platform.) Publishing / verification is optional for a private hub.</li>",
91951
- "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
91952
- `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
91953
- "<li>Paste Client ID and Client secret below. <strong>Test connection</strong> checks those with Google (not the account yet).</li>",
91954
- "<li>Add the broker — the hub then opens Google sign-in in a popup to link the account (Calendar + Gmail). Use <strong>Sign in</strong> on the row if you skip or cancel.</li>",
91955
- "</ol>"
91956
- ].join("");
91957
- }
91958
- function buildGoogleBrokerSettingsSchema(publicOrigin) {
91959
- return { sections: [
91960
- {
91961
- id: "setup-intro",
91962
- title: "Setup",
91963
- titleKey: "google-services:googleBrokerSettings.s_setup-intro.title",
91964
- description: "Google Cloud Console — register the hub redirect before Add / Sign in.",
91965
- descriptionKey: "google-services:googleBrokerSettings.s_setup-intro.description",
91966
- columns: 1,
91967
- fields: [{
91968
- type: "info",
91969
- key: "_setupSteps",
91970
- label: "Steps",
91971
- labelKey: "google-services:googleBrokerSettings.s_setup-intro._setupSteps.label",
91972
- format: "html",
91973
- content: googleBrokerSetupHtml(publicOrigin)
91974
- }]
91975
- },
91976
- {
91977
- id: "google-oauth-client",
91978
- title: "Google OAuth client",
91979
- titleKey: "google-services:googleBrokerSettings.s_google-oauth-client.title",
91980
- description: "Web application client from Google Cloud Console. The same Cloud project can be reused across Google brokers.",
91981
- descriptionKey: "google-services:googleBrokerSettings.s_google-oauth-client.description",
91982
- columns: 1,
91983
- fields: [{
91984
- type: "text",
91985
- key: "clientId",
91986
- label: "Client ID",
91987
- labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientId.label"
91988
- }, {
91989
- type: "password",
91990
- key: "clientSecret",
91991
- label: "Client secret",
91992
- labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientSecret.label",
91993
- showToggle: true
91994
- }]
91995
- },
91996
- {
91997
- id: "gmail-send",
91998
- title: "Gmail send",
91999
- titleKey: "google-services:googleBrokerSettings.s_gmail-send.title",
92000
- description: "From must be this connected account or a verified send-as. Disable Email on Connectivity to turn Gmail off without removing the broker.",
92001
- descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.description",
92002
- columns: 1,
92003
- fields: [{
92004
- type: "text",
92005
- key: "fromAddress",
92006
- label: "From address",
92007
- labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.label",
92008
- description: "Defaults to the connected Google account email when empty.",
92009
- descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.description"
92010
- }, {
92011
- type: "text",
92012
- key: "fromName",
92013
- label: "From display name (optional)",
92014
- labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromName.label"
92015
- }]
92016
- }
92017
- ] };
92018
- }
92019
- /** Connectivity Calendars / Email pages — the calendar import picker is on each broker. */
92020
- function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
92021
- const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
92022
- if (cap === "calendar-source") return { sections: [{
92023
- id: "setup-intro",
92024
- title: "Google brokers",
92025
- titleKey: "google-services:googleFeatureSettings.s_setup-intro.title",
92026
- columns: 1,
92027
- fields: [{
92028
- type: "info",
92029
- key: "_setupSteps",
92030
- label: "Setup",
92031
- labelKey: "google-services:googleFeatureSettings.s_setup-intro._setupSteps.label",
92032
- format: "html",
92033
- content: [
92034
- "<p>Link a Google account under Connectivity → <strong>Brokers</strong> ",
92035
- "(Client ID / secret + Sign in), then add a calendar under Connectivity → <strong>Calendars</strong>. ",
92036
- `OAuth redirect URI: <code>${redirect}</code>.</p>`
92037
- ].join("")
92038
- }]
92039
- }] };
92040
- return { sections: [{
92041
- id: "setup-intro",
92042
- title: cap === "smtp-provider" ? "Gmail send uses Google brokers" : "Google brokers",
92043
- columns: 1,
92044
- fields: [{
92045
- type: "info",
92046
- key: "_setupSteps",
92047
- label: "Setup",
92048
- labelKey: "google-services:googleFeatureSettings._setupSteps.label",
92049
- format: "html",
92050
- content: cap === "smtp-provider" ? [
92051
- "<p>Add or edit a <strong>Google</strong> broker under Connectivity → <strong>Brokers</strong> ",
92052
- "(Client ID / secret + Sign in). From address and send-as live on that broker. ",
92053
- `OAuth redirect URI: <code>${redirect}</code>.</p>`
92054
- ].join("") : [
92055
- "<p>Add or edit a <strong>Google</strong> broker under Connectivity → <strong>Brokers</strong> ",
92056
- "(Client ID / secret + Sign in). ",
92057
- `OAuth redirect URI: <code>${redirect}</code>.</p>`
92058
- ].join("")
92059
- }]
92060
- }] };
92061
- }
92062
- function escapeHtml(s) {
92063
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92064
- }
92065
- //#endregion
92066
91773
  //#region src/google-calendar-source.ts
92067
91774
  function parseGoogleInstant(value) {
92068
91775
  const ms = Date.parse(value);
@@ -92559,7 +92266,13 @@ function buildGoogleCalendarInstances(records, deps) {
92559
92266
  role: "services",
92560
92267
  instanceMode: "multiple",
92561
92268
  kind: GOOGLE_CALENDAR_INSTANCE_KIND,
92562
- canTest: false
92269
+ canTest: false,
92270
+ kinds: [{
92271
+ id: GOOGLE_CALENDAR_INSTANCE_KIND,
92272
+ label: "Google calendar",
92273
+ functionCap: "calendar-source",
92274
+ role: "services"
92275
+ }]
92563
92276
  },
92564
92277
  labels: { accountId: {
92565
92278
  label: "Google account",
@@ -92583,6 +92296,179 @@ function buildGoogleCalendarInstances(records, deps) {
92583
92296
  });
92584
92297
  }
92585
92298
  //#endregion
92299
+ //#region src/google-settings-copy.ts
92300
+ /**
92301
+ * Operator-facing setup copy for Connectivity → Calendars / Email.
92302
+ * Keep Connect as a same-origin relative link so the admin session cookie
92303
+ * rides along (an absolute Public URL from a LAN admin tab drops the cookie).
92304
+ * Root-relative hrefs must survive `sanitizeInfoHtml` in ui-library.
92305
+ */
92306
+ /** Enable APIs library in Google Cloud Console. */
92307
+ var GOOGLE_CONSOLE_APIS_URL = "https://console.cloud.google.com/apis/library";
92308
+ /** Create / manage OAuth Web clients. */
92309
+ var GOOGLE_CONSOLE_CREDENTIALS_URL = "https://console.cloud.google.com/apis/credentials";
92310
+ function redirectUriDisplay(publicOrigin) {
92311
+ if (publicOrigin === null || publicOrigin.length === 0) return `<public-hub>${OAUTH_CALLBACK_SUFFIX}`;
92312
+ return `${publicOrigin.replace(/\/$/, "")}${OAUTH_CALLBACK_SUFFIX}`;
92313
+ }
92314
+ function consoleLinksHtml$1() {
92315
+ return [
92316
+ `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
92317
+ ` · `,
92318
+ `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
92319
+ ].join("");
92320
+ }
92321
+ function introHtml(cap, publicOrigin) {
92322
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
92323
+ const console = consoleLinksHtml$1();
92324
+ if (cap === "smtp-provider") return [
92325
+ "<ol>",
92326
+ `<li>Open Google Cloud Console (${console}) and enable the <strong>Gmail API</strong> (and Calendar API if you also use Calendars).</li>`,
92327
+ "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
92328
+ `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
92329
+ "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
92330
+ `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
92331
+ "<li>Set From address (connected account or a verified send-as). Turn this integration off with the page <strong>Enable</strong> toggle when you do not want Gmail send — Calendar can stay connected.</li>",
92332
+ "</ol>"
92333
+ ].join("");
92334
+ if (cap === "calendar-source") return [
92335
+ "<ol>",
92336
+ `<li>Open Google Cloud Console (${console}) and enable the <strong>Google Calendar API</strong> (and Gmail API if you also use Email).</li>`,
92337
+ "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
92338
+ `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
92339
+ "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
92340
+ `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
92341
+ "<li>Optionally restrict which calendars are exposed with the filter below.</li>",
92342
+ "</ol>"
92343
+ ].join("");
92344
+ return [
92345
+ "<ol>",
92346
+ `<li>Open Google Cloud Console (${console}) and enable <strong>Google Calendar API</strong> and <strong>Gmail API</strong>.</li>`,
92347
+ "<li>Create a <strong>Web application</strong> OAuth client under Credentials.</li>",
92348
+ `<li>Register redirect URI: <code>${redirect}</code> (Network → Public endpoint).</li>`,
92349
+ `<li>Paste Client ID / secret, Save, then <a href="${OAUTH_START_PATH}">Connect Google</a>.</li>`,
92350
+ "<li>Calendars and Email share one Google account; use each page’s <strong>Enable</strong> toggle to turn a capability off without revoking OAuth.</li>",
92351
+ "</ol>"
92352
+ ].join("");
92353
+ }
92354
+ function connectHtml(cap, publicOrigin) {
92355
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
92356
+ if (cap === "smtp-provider") return [
92357
+ `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
92358
+ "Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
92359
+ `Google must list exactly: <code>${redirect}</code>. `,
92360
+ "If an older Connect predates Gmail send, revoke CamStack in Google Account → Security and Connect again. ",
92361
+ `Console: ${consoleLinksHtml$1()}.`
92362
+ ].join("");
92363
+ if (cap === "calendar-source") return [
92364
+ `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
92365
+ "Connect requests <code>calendar.readonly</code> (and Gmail send for the Email page). ",
92366
+ `Google must list exactly: <code>${redirect}</code>. `,
92367
+ `Console: ${consoleLinksHtml$1()}.`
92368
+ ].join("");
92369
+ return [
92370
+ `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a>. `,
92371
+ `Redirect URI registered in Google Cloud must be <code>${redirect}</code>. `,
92372
+ `Console: ${consoleLinksHtml$1()}.`
92373
+ ].join("");
92374
+ }
92375
+ function escapeHtml$1(s) {
92376
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92377
+ }
92378
+ //#endregion
92379
+ //#region src/google-broker-settings-schema.ts
92380
+ /**
92381
+ * Per-kind broker creation/edit form for kind=`google`.
92382
+ * Account linking (Sign in) is driven by the kind's `connectPath`
92383
+ * on the instances page — not a hard-coded Connect link in this form.
92384
+ */
92385
+ function consoleLinksHtml() {
92386
+ return [
92387
+ `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
92388
+ ` · `,
92389
+ `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
92390
+ ].join("");
92391
+ }
92392
+ /** Setup steps shown on Add / Edit — includes Audience / Test users. */
92393
+ function googleBrokerSetupHtml(publicOrigin) {
92394
+ const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
92395
+ return [
92396
+ "<ol>",
92397
+ `<li>Open Google Cloud Console (${consoleLinksHtml()}). Create a <strong>new project</strong> (or pick an existing one) from the project picker at the top.</li>`,
92398
+ "<li>In that project, enable <strong>Google Calendar API</strong> and/or <strong>Gmail API</strong> (API library).</li>",
92399
+ "<li>Open <strong>Google Auth Platform → Audience</strong> (or APIs &amp; Services → OAuth consent screen → <strong>Audience</strong>). User type <strong>External</strong> + publishing status <strong>Testing</strong> is fine for a personal hub. Under <strong>Test users</strong>, add the Google account(s) that will Sign in — otherwise Google returns <code>403: access_denied</code>. (If you only see Branding / Data Access, open the <strong>Audience</strong> tab in the left sidebar of Google Auth Platform.) Publishing / verification is optional for a private hub.</li>",
92400
+ "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
92401
+ `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
92402
+ "<li>Paste Client ID and Client secret below. <strong>Test connection</strong> checks those with Google (not the account yet).</li>",
92403
+ "<li>Add the broker — the hub then opens Google sign-in in a popup to link the account (Calendar + Gmail). Use <strong>Sign in</strong> on the row if you skip or cancel.</li>",
92404
+ "</ol>"
92405
+ ].join("");
92406
+ }
92407
+ function buildGoogleBrokerSettingsSchema(publicOrigin) {
92408
+ return { sections: [
92409
+ {
92410
+ id: "setup-intro",
92411
+ title: "Setup",
92412
+ titleKey: "google-services:googleBrokerSettings.s_setup-intro.title",
92413
+ description: "Google Cloud Console — register the hub redirect before Add / Sign in.",
92414
+ descriptionKey: "google-services:googleBrokerSettings.s_setup-intro.description",
92415
+ columns: 1,
92416
+ fields: [{
92417
+ type: "info",
92418
+ key: "_setupSteps",
92419
+ label: "Steps",
92420
+ labelKey: "google-services:googleBrokerSettings.s_setup-intro._setupSteps.label",
92421
+ format: "html",
92422
+ content: googleBrokerSetupHtml(publicOrigin)
92423
+ }]
92424
+ },
92425
+ {
92426
+ id: "google-oauth-client",
92427
+ title: "Google OAuth client",
92428
+ titleKey: "google-services:googleBrokerSettings.s_google-oauth-client.title",
92429
+ description: "Web application client from Google Cloud Console. The same Cloud project can be reused across Google brokers.",
92430
+ descriptionKey: "google-services:googleBrokerSettings.s_google-oauth-client.description",
92431
+ columns: 1,
92432
+ fields: [{
92433
+ type: "text",
92434
+ key: "clientId",
92435
+ label: "Client ID",
92436
+ labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientId.label"
92437
+ }, {
92438
+ type: "password",
92439
+ key: "clientSecret",
92440
+ label: "Client secret",
92441
+ labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientSecret.label",
92442
+ showToggle: true
92443
+ }]
92444
+ },
92445
+ {
92446
+ id: "gmail-send",
92447
+ title: "Gmail send",
92448
+ titleKey: "google-services:googleBrokerSettings.s_gmail-send.title",
92449
+ description: "From must be this connected account or a verified send-as. Disable Email on Connectivity to turn Gmail off without removing the broker.",
92450
+ descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.description",
92451
+ columns: 1,
92452
+ fields: [{
92453
+ type: "text",
92454
+ key: "fromAddress",
92455
+ label: "From address",
92456
+ labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.label",
92457
+ description: "Defaults to the connected Google account email when empty.",
92458
+ descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.description"
92459
+ }, {
92460
+ type: "text",
92461
+ key: "fromName",
92462
+ label: "From display name (optional)",
92463
+ labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromName.label"
92464
+ }]
92465
+ }
92466
+ ] };
92467
+ }
92468
+ function escapeHtml(s) {
92469
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92470
+ }
92471
+ //#endregion
92586
92472
  //#region src/google-instances.ts
92587
92473
  /**
92588
92474
  * The Google accounts as `integration-instances` (planning#408 P3): one instance per broker
@@ -92619,7 +92505,8 @@ function buildGoogleInstances(records, deps) {
92619
92505
  instanceMode: "multiple",
92620
92506
  kind: "account",
92621
92507
  canTest: true,
92622
- allowRestoreId: true
92508
+ allowRestoreId: true,
92509
+ kinds: [googleAccountKind(deps)]
92623
92510
  },
92624
92511
  formSchema: () => buildGoogleBrokerSettingsSchema(deps.publicOrigin()),
92625
92512
  initialBody: (name) => ({
@@ -93869,22 +93756,7 @@ var GoogleServicesAddon = class extends BaseAddon {
93869
93756
  this.publicOrigin = null;
93870
93757
  }
93871
93758
  }
93872
- /**
93873
- * Connectivity Calendars / Email: point at Brokers (no duplicate OAuth fields).
93874
- * Addon page without cap: same pointer + note.
93875
- */
93876
- globalSettingsSchema(cap) {
93877
- const scoped = asSettingsCap(cap);
93878
- return this.schema(buildGoogleFeatureSettingsSchema(scoped, this.publicOrigin));
93879
- }
93880
- async getGlobalSettings(overlay, cap, nodeId) {
93881
- await this.refreshPublicOrigin();
93882
- return super.getGlobalSettings(overlay, cap, nodeId);
93883
- }
93884
93759
  };
93885
- function asSettingsCap(cap) {
93886
- if (cap === "calendar-source" || cap === "smtp-provider") return cap;
93887
- }
93888
93760
  //#endregion
93889
93761
  exports.DEFAULT_GOOGLE_CONFIG = DEFAULT_GOOGLE_CONFIG;
93890
93762
  exports.GOOGLE_BROKER_KIND = GOOGLE_BROKER_KIND;