@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.
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- //#region ../types/dist/event-category-BmPm98n0.mjs
2
+ //#region ../types/dist/event-category-78tcBemo.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -120,6 +120,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
120
120
  */
121
121
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
122
122
  /**
123
+ * An addon is RUNNING although its installed package is below what the
124
+ * running server requires (`camstack.requires.addons` of the server, or the
125
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
126
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
127
+ * operator alert per node and package. The addon is never held back for it.
128
+ *
129
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
130
+ */
131
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
132
+ /**
133
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
134
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
135
+ *
136
+ * Payload: `{ packageName, addonId, nodeId }`.
137
+ */
138
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
139
+ /**
123
140
  * Monitor scheduled the next retry for a failed addon. Transient —
124
141
  * surfaced to the UI for live-updating the "next retry in Ns"
125
142
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -230,7 +247,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
230
247
  EventCategory["ProcessRestarted"] = "process.restarted";
231
248
  /**
232
249
  * The SET of storage locations changed — one was created, edited, enabled,
233
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
250
+ * disabled or deleted through the location instances or the forced-delete action.
234
251
  *
235
252
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
236
253
  * it must also converge on its own periodic path, because a dropped event
@@ -785,7 +802,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
785
802
  return EventCategory;
786
803
  }({});
787
804
  //#endregion
788
- //#region ../types/dist/device-type-D1fH5kaC.mjs
805
+ //#region ../types/dist/device-type-DxCT0krO.mjs
789
806
  /**
790
807
  import { errMsg } from '@camstack/types'
791
808
  * Extract a human-readable message from an unknown error value.
@@ -947,6 +964,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
947
964
  * filters that want "all devices with on-motion behaviour".
948
965
  */
949
966
  DeviceFeature["MotionTrigger"] = "motion-trigger";
967
+ /**
968
+ * Device can be run for a chosen time and then stopped. Mirrors the
969
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
970
+ * a driver, so the flag is derived from the device's bindings
971
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
972
+ */
973
+ DeviceFeature["TimedRun"] = "timed-run";
950
974
  /** Light supports rgb-triplet color via `color` cap. */
951
975
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
952
976
  /** Light supports HSV color via `color` cap. */
@@ -5981,7 +6005,7 @@ var ZodIssueCode = {
5981
6005
  var ZodFirstPartyTypeKind;
5982
6006
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5983
6007
  //#endregion
5984
- //#region ../types/dist/sleep-D7ZKm4nb.mjs
6008
+ //#region ../types/dist/sleep-CGPZyzB5.mjs
5985
6009
  /**
5986
6010
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5987
6011
  * window to float samples (D455).
@@ -6345,6 +6369,59 @@ Object.fromEntries([
6345
6369
  function isValuelessField(field) {
6346
6370
  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";
6347
6371
  }
6372
+ /**
6373
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6374
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6375
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6376
+ * pre-resolving) lets callers both read the effective value AND detect
6377
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6378
+ * malformed input yields an empty map, never throws.
6379
+ */
6380
+ function collectHydratedFieldEntries(schema) {
6381
+ const out = /* @__PURE__ */ new Map();
6382
+ if (typeof schema !== "object" || schema === null) return out;
6383
+ const sections = schema.sections;
6384
+ if (!Array.isArray(sections)) return out;
6385
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6386
+ return out;
6387
+ }
6388
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6389
+ function collectFieldEntriesInto(fields, into) {
6390
+ if (!Array.isArray(fields)) return;
6391
+ for (const field of fields) {
6392
+ if (typeof field !== "object" || field === null) continue;
6393
+ const type = field.type;
6394
+ if (type === "group") {
6395
+ collectFieldEntriesInto(field.fields, into);
6396
+ continue;
6397
+ }
6398
+ if (type === "sub-tabs") {
6399
+ const tabs = field.tabs;
6400
+ if (Array.isArray(tabs)) {
6401
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6402
+ }
6403
+ continue;
6404
+ }
6405
+ const key = field.key;
6406
+ if (typeof key !== "string" || key.length === 0) continue;
6407
+ if (!("value" in field)) continue;
6408
+ into.set(key, {
6409
+ value: field.value,
6410
+ default: field.default
6411
+ });
6412
+ }
6413
+ }
6414
+ /**
6415
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6416
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6417
+ * common "just give me the resolved values" case. Built on
6418
+ * {@link collectHydratedFieldEntries}.
6419
+ */
6420
+ function collectHydratedFieldValues(schema) {
6421
+ const out = /* @__PURE__ */ new Map();
6422
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6423
+ return out;
6424
+ }
6348
6425
  function hydrateSchema(schema, values, options = {}) {
6349
6426
  return {
6350
6427
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -7145,6 +7222,7 @@ var RecordIntegrationInstances = class {
7145
7222
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
7146
7223
  this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
7147
7224
  this.nameField = kind.declaration.entry?.nameField ?? "name";
7225
+ 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`);
7148
7226
  }
7149
7227
  /**
7150
7228
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -7186,10 +7264,21 @@ var RecordIntegrationInstances = class {
7186
7264
  }
7187
7265
  return out;
7188
7266
  }
7267
+ /** The kind an instance body is of: the provider's `kindOf`, else its one declared kind. */
7268
+ kindOfBody(body) {
7269
+ if (this.hooks.kindOf !== void 0) {
7270
+ const id = this.hooks.kindOf(body);
7271
+ 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)`);
7272
+ return id;
7273
+ }
7274
+ const only = this.hooks.description.kinds[0];
7275
+ if (only === void 0) throw new Error(`record kind '${this.kind.name}' declares no kinds`);
7276
+ return only.id;
7277
+ }
7189
7278
  viewOf(record) {
7190
7279
  const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
7191
7280
  const rawName = record.data[this.nameField];
7192
- const kind = this.hooks.kindOf?.(record.data);
7281
+ const kind = this.kindOfBody(record.data);
7193
7282
  const health = this.hooks.health?.(record.data, record.id);
7194
7283
  return {
7195
7284
  id: record.id,
@@ -7197,7 +7286,7 @@ var RecordIntegrationInstances = class {
7197
7286
  configured: this.hooks.configured(record.data),
7198
7287
  enabled: isInstanceEnabled(record.data),
7199
7288
  formValues: shown,
7200
- ...kind !== void 0 ? { kind } : {},
7289
+ kind,
7201
7290
  ...health?.status !== void 0 ? { status: health.status } : {},
7202
7291
  ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
7203
7292
  };
@@ -7230,17 +7319,35 @@ var RecordIntegrationInstances = class {
7230
7319
  ...kind !== void 0 ? { kind } : {}
7231
7320
  });
7232
7321
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7322
+ const initial = this.hooks.initialBody(name, kind);
7323
+ const patch = this.patchOf(values ?? {});
7324
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7325
+ kind,
7326
+ body: {
7327
+ ...initial,
7328
+ ...patch
7329
+ }
7330
+ }));
7233
7331
  await this.handle.put({
7234
7332
  id: minted,
7235
- data: this.hooks.initialBody(name, kind)
7333
+ data: initial
7236
7334
  }, { ifRev: 0 });
7237
- const patch = this.patchOf(values ?? {});
7238
7335
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7239
7336
  await this.hooks.onChanged?.(minted, "created");
7240
7337
  return { id: minted };
7241
7338
  };
7242
7339
  updateInstance = async ({ id, name, values }) => {
7243
7340
  const patch = this.patchOf(values);
7341
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7342
+ const stored = await this.handle.get(id);
7343
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7344
+ kind: void 0,
7345
+ body: {
7346
+ ...stored.data,
7347
+ ...patch
7348
+ }
7349
+ }));
7350
+ }
7244
7351
  if (name !== void 0) patch[this.nameField] = name;
7245
7352
  await this.handle.patch(id, patch);
7246
7353
  await this.hooks.onChanged?.(id, "updated");
@@ -7551,6 +7658,21 @@ var SCOPED_SETTINGS_SCOPES = [
7551
7658
  "device-defaults",
7552
7659
  "node"
7553
7660
  ];
7661
+ /** The one refusal of a key a scope did not declare. */
7662
+ function assertDeclaredKeys(keys, declared) {
7663
+ const undeclared = keys.filter((key) => !declared.has(key));
7664
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7665
+ }
7666
+ /** Build the handle over an addon's scoped read, write and unset. */
7667
+ function createScopedSettings(deps) {
7668
+ return {
7669
+ async get() {
7670
+ return deps.schema.parse(await deps.read());
7671
+ },
7672
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7673
+ unset: (keys) => deps.unset(keys)
7674
+ };
7675
+ }
7554
7676
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7555
7677
  var KeychainWriteRefusedError = class extends Error {
7556
7678
  fieldPath;
@@ -8126,7 +8248,16 @@ function createHandle(raw, opts) {
8126
8248
  return blob;
8127
8249
  }
8128
8250
  },
8129
- writeAddonStore: writeAddon
8251
+ writeAddonStore: writeAddon,
8252
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8253
+ const stored = await readStoredAddonBlob(raw);
8254
+ index.seen("addon", stored);
8255
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8256
+ if (removable.length === 0) return;
8257
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8258
+ await raw.writeAddonStore(patch);
8259
+ index.wrote("addon", patch);
8260
+ })
8130
8261
  };
8131
8262
  }
8132
8263
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8256,7 +8387,7 @@ function boundOf(list) {
8256
8387
  * Base class for CamStack addons. Eliminates settings boilerplate:
8257
8388
  *
8258
8389
  * - Typed `config` property with automatic resolution from store + defaults
8259
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8390
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8260
8391
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8261
8392
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8262
8393
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8266,7 +8397,7 @@ function boundOf(list) {
8266
8397
  * Subclasses override:
8267
8398
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8268
8399
  * - `onShutdown()` — cleanup
8269
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8400
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8270
8401
  *
8271
8402
  * @example
8272
8403
  * ```ts
@@ -8280,7 +8411,7 @@ function boundOf(list) {
8280
8411
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8281
8412
  * }
8282
8413
  *
8283
- * protected globalSettingsSchema() {
8414
+ * protected clusterSettingsSchema() {
8284
8415
  * return this.schema({
8285
8416
  * sections: [{
8286
8417
  * id: 'main', title: 'Settings',
@@ -8323,7 +8454,7 @@ var BaseAddon = class {
8323
8454
  defaults;
8324
8455
  /**
8325
8456
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8326
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8457
+ * itself (`state()`, the scoped writers) that no form field declares.
8327
8458
  * Stored in the keychain exactly like a `password` field. Syntax:
8328
8459
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8329
8460
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8333,7 +8464,7 @@ var BaseAddon = class {
8333
8464
  /**
8334
8465
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8335
8466
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8336
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8467
+ * see them. Empty until loaded.
8337
8468
  */
8338
8469
  _scopedByCap = /* @__PURE__ */ new Map();
8339
8470
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8519,7 +8650,7 @@ var BaseAddon = class {
8519
8650
  /** Addon-specific cleanup. Override if needed. */
8520
8651
  async onShutdown() {}
8521
8652
  /**
8522
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8653
+ * Called after config is resolved during a settings write.
8523
8654
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8524
8655
  * Not called during initialize() — use onInitialize() for initial setup.
8525
8656
  */
@@ -8548,82 +8679,12 @@ var BaseAddon = class {
8548
8679
  scopedSettingsCapabilities() {
8549
8680
  return [];
8550
8681
  }
8551
- /** Override to provide global-level settings UI schema. */
8552
- globalSettingsSchema(_cap) {
8553
- return null;
8554
- }
8555
8682
  /** Override to provide device-level settings UI schema. */
8556
8683
  deviceSettingsSchema() {
8557
8684
  return null;
8558
8685
  }
8559
8686
  /**
8560
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8561
- * ARE the configuration of its integration.
8562
- *
8563
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8564
- * operator should find on the addon's integration page (System →
8565
- * Integrations → <name>) rather than only in the cluster-wide list of every
8566
- * addon. Empty (the default) means the addon has no integration-level
8567
- * settings and no such surface is offered — this is opt-in, because whether
8568
- * an addon's configuration IS its integration's configuration depends on the
8569
- * nature of the integration.
8570
- *
8571
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8572
- * the ONE global schema, in the ONE addon store, written by the ONE
8573
- * `updateGlobalSettings` path. There is deliberately no
8574
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8575
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8576
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8577
- *
8578
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8579
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8580
- * removed with the reason recorded at
8581
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8582
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8583
- * marker sprinkled across sections also has to borrow a field that already
8584
- * means something else; borrowing `section.tab` put the literal word
8585
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8586
- * GROUP this visually" and cannot also mean "where this lives" (D269
8587
- * supersedes D268). One declaration, in one place, next to the schema whose
8588
- * ids it names.
8589
- */
8590
- integrationSettingSections() {
8591
- return [];
8592
- }
8593
- async getGlobalSettings(overlay, cap, nodeId) {
8594
- await this.loadScopedSchemas(cap);
8595
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8596
- if (!schema) return { sections: [] };
8597
- const projected = await this.readRawProjectedStore(nodeId, cap);
8598
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8599
- }
8600
- /**
8601
- * The integration-level view of this addon's settings: exactly the sections
8602
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8603
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8604
- *
8605
- * Returns `null` when the addon declared nothing — an addon that opts out has
8606
- * no integration settings surface at all, rather than an empty one that reads
8607
- * as a failed load.
8608
- *
8609
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8610
- * and not in whichever UI happens to render this:
8611
- *
8612
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8613
- * shown here is the same field, with the same bare key, that the addon's
8614
- * own page shows. There is no integration-specific writer — callers save
8615
- * through `updateGlobalSettings` — so a second store key is unreachable,
8616
- * not merely discouraged.
8617
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8618
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8619
- * such a field silently picked would be a wrong answer for the operator
8620
- * who opened the page (D266).
8621
- * 3. **No silent typo.** A declared id that names no section throws. The
8622
- * alternative — skip it — turns a rename into a surface that quietly
8623
- * empties, which looks exactly like an addon with nothing to configure.
8624
- */
8625
- /**
8626
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8687
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8627
8688
  *
8628
8689
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8629
8690
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8671,37 +8732,178 @@ var BaseAddon = class {
8671
8732
  }
8672
8733
  }
8673
8734
  /**
8735
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8736
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8737
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8738
+ */
8739
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8740
+ return null;
8741
+ }
8742
+ /**
8743
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8744
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8745
+ */
8746
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8747
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8748
+ clusterSettings(cap, schema) {
8749
+ return this.scopedHandle("cluster", cap, schema);
8750
+ }
8751
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8752
+ deviceDefaults(cap, schema) {
8753
+ return this.scopedHandle("device-defaults", cap, schema);
8754
+ }
8755
+ /**
8756
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8757
+ * with no row yields the schema defaults — never a bare-key fallback.
8758
+ */
8759
+ nodeSettings(cap, schema, nodeId) {
8760
+ return this.scopedHandle("node", cap, schema, nodeId);
8761
+ }
8762
+ /**
8763
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8764
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8765
+ * central delete. `nodeId` omitted ⇒ the local node.
8766
+ */
8767
+ nodeState(key, schema, fallback, nodeId) {
8768
+ this._declaredStateKeys.add(key);
8769
+ return createDurableState({
8770
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8771
+ schema,
8772
+ fallback,
8773
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8774
+ write: async (patch) => {
8775
+ await this._ctx?.settings?.writeAddonStore(patch);
8776
+ },
8777
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8778
+ addonId: this._ctx?.id,
8779
+ key: k,
8780
+ error: String(e)
8781
+ } })
8782
+ });
8783
+ }
8784
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8785
+ async declaredScopedSchema(scope, cap) {
8786
+ await this.loadScopedSchemas(cap);
8787
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8788
+ }
8789
+ async requireScopedSchema(scope, cap) {
8790
+ const schema = await this.declaredScopedSchema(scope, cap);
8791
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8792
+ return schema;
8793
+ }
8794
+ scopedKeys(schema) {
8795
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8796
+ }
8797
+ /** `store` narrowed to the keys `schema` declares. */
8798
+ narrowToScope(store, schema) {
8799
+ const keys = this.scopedKeys(schema);
8800
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8801
+ }
8802
+ scopedHandle(scope, cap, schema, nodeId) {
8803
+ return createScopedSettings({
8804
+ schema,
8805
+ read: async () => {
8806
+ const declared = await this.requireScopedSchema(scope, cap);
8807
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8808
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8809
+ return {
8810
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8811
+ ...stored
8812
+ };
8813
+ },
8814
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8815
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8816
+ });
8817
+ }
8818
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8819
+ async writeScoped(scope, cap, patch, nodeId) {
8820
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8821
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8822
+ }
8823
+ /**
8824
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8825
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8826
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8827
+ */
8828
+ async unsetScoped(scope, cap, keys, nodeId) {
8829
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8830
+ const target = scope === "node" ? nodeId : void 0;
8831
+ const tree = this.addonSecretTree();
8832
+ const secretKeys = keys.filter((key) => tree.has(key));
8833
+ const plainKeys = keys.filter((key) => !tree.has(key));
8834
+ let changed = false;
8835
+ if (plainKeys.length > 0) {
8836
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8837
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8838
+ }
8839
+ if (secretKeys.length > 0) {
8840
+ await this.clearSecretKeys(secretKeys, target, cap);
8841
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8842
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8843
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8844
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8845
+ changed = true;
8846
+ }
8847
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8848
+ }
8849
+ /**
8850
+ * The hydrated form of one scope of one capability's settings (any
8851
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8852
+ */
8853
+ async getScopedSettings(scope, cap, nodeId) {
8854
+ const schema = await this.declaredScopedSchema(scope, cap);
8855
+ if (schema === null) return null;
8856
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8857
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8858
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8859
+ }
8860
+ /**
8861
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8862
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8863
+ * nothing.
8864
+ */
8865
+ async listScopedSettings() {
8866
+ await this.loadScopedSchemas();
8867
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8868
+ cap,
8869
+ scope: entry.scope,
8870
+ sectionIds: entry.schema.sections.map((section) => section.id)
8871
+ })));
8872
+ }
8873
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8874
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8875
+ this._operatorWriteDepth++;
8876
+ try {
8877
+ await this.writeScoped(scope, cap, patch, nodeId);
8878
+ } finally {
8879
+ this._operatorWriteDepth--;
8880
+ }
8881
+ }
8882
+ /**
8883
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8884
+ * its value. `null` when the capability declares nothing for the scope.
8885
+ */
8886
+ async readScopedValues(scope, cap, nodeId) {
8887
+ const schema = await this.declaredScopedSchema(scope, cap);
8888
+ if (schema === null) return null;
8889
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8890
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8891
+ }
8892
+ /**
8674
8893
  * What the create wizard and the integration edit page render for one instance: the addon's
8675
8894
  * per-instance schema when it declares one, else its global schema (what they always showed).
8676
8895
  */
8677
8896
  async getIntegrationFormSchema(instance, nodeId) {
8678
8897
  const scoped = await this.integrationFormSchema(instance);
8679
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8898
+ if (scoped === null) return this.scopedForm(nodeId);
8680
8899
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8681
8900
  }
8682
- async getIntegrationSettings(nodeId) {
8683
- const declared = this.integrationSettingSections();
8684
- if (declared.length === 0) return null;
8685
- const schema = this.globalSettingsSchema();
8686
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8687
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8688
- const sections = [];
8689
- for (const id of declared) {
8690
- const section = byId.get(id);
8691
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8692
- const fields = dropPerNodeFields(section.fields);
8693
- if (fields.length === 0) continue;
8694
- sections.push({
8695
- ...section,
8696
- fields
8697
- });
8698
- }
8699
- if (sections.length === 0) return null;
8700
- const projected = await this.readRawProjectedStore(nodeId);
8701
- return hydrateSchema({
8702
- ...schema,
8703
- sections
8704
- }, projected, { inlineSecretsAsViews: true });
8901
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8902
+ async scopedForm(nodeId) {
8903
+ await this.loadScopedSchemas();
8904
+ const schema = this.scopedFormSchema();
8905
+ if (!schema) return { sections: [] };
8906
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8705
8907
  }
8706
8908
  /**
8707
8909
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8710,8 +8912,8 @@ var BaseAddon = class {
8710
8912
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8711
8913
  * A no-op passthrough when the schema declares no `perNode` field.
8712
8914
  *
8713
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8714
- * the store for custom option logic (option narrowing, value snapping) to
8915
+ * This is the sanctioned way for an addon that needs
8916
+ * the store for custom logic (option narrowing, value snapping) to
8715
8917
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8716
8918
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8717
8919
  *
@@ -8763,7 +8965,7 @@ var BaseAddon = class {
8763
8965
  }
8764
8966
  /**
8765
8967
  * Whether ONE global key of this addon's store was written, read three-way
8766
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
8968
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8767
8969
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8768
8970
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8769
8971
  * not supported here: its stored name is scoped.
@@ -8797,7 +8999,7 @@ var BaseAddon = class {
8797
8999
  }
8798
9000
  /**
8799
9001
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8800
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9002
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8801
9003
  * the operator sends is stored as sent even when it equals a pre-keychain
8802
9004
  * inline value: the operator re-entering a password is a change, an addon
8803
9005
  * writing back what it read is not (D787).
@@ -8810,9 +9012,6 @@ var BaseAddon = class {
8810
9012
  this._operatorWriteDepth--;
8811
9013
  }
8812
9014
  }
8813
- async updateGlobalSettings(patch, nodeId) {
8814
- await this.applyGlobalPatch(patch, nodeId);
8815
- }
8816
9015
  /**
8817
9016
  * REMOVE stored secrets — their keychain values and their markers (D787).
8818
9017
  *
@@ -8822,6 +9021,9 @@ var BaseAddon = class {
8822
9021
  * a keychain (an older kernel) it writes `''`, as such code always did.
8823
9022
  */
8824
9023
  async clearSecrets(keys, nodeId) {
9024
+ await this.clearSecretKeys(keys, nodeId);
9025
+ }
9026
+ async clearSecretKeys(keys, nodeId, cap) {
8825
9027
  const tree = this.addonSecretTree();
8826
9028
  const keychain = this._ctx?.secrets !== void 0;
8827
9029
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8834,25 +9036,26 @@ var BaseAddon = class {
8834
9036
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8835
9037
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8836
9038
  }
8837
- await this.applyGlobalPatch(patch, nodeId);
9039
+ await this.applyGlobalPatch(patch, nodeId, cap);
8838
9040
  }
8839
- async applyGlobalPatch(barePatch, nodeId) {
8840
- await this.loadScopedSchemas();
8841
- const keys = this.perNodeKeys();
9041
+ /** Resolves `true` when the write changed stored values. */
9042
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9043
+ await this.loadScopedSchemas(cap);
9044
+ const keys = this.perNodeKeys(cap);
8842
9045
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8843
9046
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8844
9047
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8845
9048
  const handle = this._secretSettings;
8846
9049
  if (handle !== null) {
8847
9050
  const { changed } = await handle.writeAddonStore(scoped);
8848
- if (!changed) return;
9051
+ if (!changed) return false;
8849
9052
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8850
- if (target !== localNode) return;
9053
+ if (target !== localNode) return true;
8851
9054
  await this.resolveConfig();
8852
9055
  await this.onConfigChanged();
8853
9056
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8854
- const legacy = this.globalSettingsSchema();
8855
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9057
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9058
+ return true;
8856
9059
  }
8857
9060
  /**
8858
9061
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8889,10 +9092,7 @@ var BaseAddon = class {
8889
9092
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8890
9093
  this._addonSecretTree = null;
8891
9094
  }
8892
- /**
8893
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8894
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8895
- */
9095
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8896
9096
  scopedFormSchema(cap) {
8897
9097
  const byId = /* @__PURE__ */ new Map();
8898
9098
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8904,8 +9104,7 @@ var BaseAddon = class {
8904
9104
  return [...this._scopedByCap.values()].flat();
8905
9105
  }
8906
9106
  /**
8907
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8908
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9107
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8909
9108
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8910
9109
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8911
9110
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8914,8 +9113,7 @@ var BaseAddon = class {
8914
9113
  const cacheKey = cap ?? "";
8915
9114
  const cached = this._nodeKeysByCap.get(cacheKey);
8916
9115
  if (cached) return cached;
8917
- const legacy = this.globalSettingsSchema(cap);
8918
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9116
+ const keys = /* @__PURE__ */ new Set();
8919
9117
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8920
9118
  if (scope !== "node") continue;
8921
9119
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9133,8 +9331,6 @@ var BaseAddon = class {
9133
9331
  }
9134
9332
  declaredKeySet() {
9135
9333
  const keys = new Set(Object.keys(this.defaults));
9136
- const schema = this.globalSettingsSchema();
9137
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9138
9334
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9139
9335
  for (const key of this._declaredStateKeys) keys.add(key);
9140
9336
  for (const path of this.secretStatePaths) {
@@ -9232,7 +9428,7 @@ var BaseAddon = class {
9232
9428
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9233
9429
  addonSecretTree() {
9234
9430
  if (this._addonSecretTree === null) {
9235
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9431
+ const tree = /* @__PURE__ */ new Map();
9236
9432
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9237
9433
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9238
9434
  }
@@ -9444,47 +9640,6 @@ function classifyStoreError(err) {
9444
9640
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9445
9641
  return "permanent";
9446
9642
  }
9447
- /**
9448
- * Collect the keys of every field marked `perNode: true`, recursing into
9449
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9450
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9451
- * don't declare `perNode` and are excluded by the `in` narrowing.
9452
- */
9453
- /**
9454
- * The same fields with every `perNode: true` one removed, recursing into layout
9455
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9456
- * with no child is dropped rather than rendered empty.
9457
- *
9458
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9459
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9460
- */
9461
- function dropPerNodeFields(fields) {
9462
- const kept = [];
9463
- for (const field of fields) {
9464
- if (field.type === "group") {
9465
- const inner = dropPerNodeFields(field.fields);
9466
- if (inner.length > 0) kept.push({
9467
- ...field,
9468
- fields: inner
9469
- });
9470
- continue;
9471
- }
9472
- if (field.type === "sub-tabs") {
9473
- const tabs = field.tabs.map((tab) => ({
9474
- ...tab,
9475
- fields: dropPerNodeFields(tab.fields)
9476
- })).filter((tab) => tab.fields.length > 0);
9477
- if (tabs.length > 0) kept.push({
9478
- ...field,
9479
- tabs
9480
- });
9481
- continue;
9482
- }
9483
- if ("perNode" in field && field.perNode === true) continue;
9484
- kept.push(field);
9485
- }
9486
- return kept;
9487
- }
9488
9643
  /** The keys of the value-holding fields, recursing into layout containers. */
9489
9644
  function collectValueFieldKeys(fields) {
9490
9645
  const collected = [];
@@ -9517,21 +9672,6 @@ function collectFieldKeys(fields) {
9517
9672
  }
9518
9673
  return collected;
9519
9674
  }
9520
- function collectPerNodeFieldKeys(fields) {
9521
- const collected = [];
9522
- for (const field of fields) {
9523
- if (field.type === "group") {
9524
- collected.push(...collectPerNodeFieldKeys(field.fields));
9525
- continue;
9526
- }
9527
- if (field.type === "sub-tabs") {
9528
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9529
- continue;
9530
- }
9531
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9532
- }
9533
- return collected;
9534
- }
9535
9675
  /**
9536
9676
  * Normalize an `ICamstackAddon.initialize()` return value into the
9537
9677
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10582,7 +10722,6 @@ var CAP_NAMES = Object.freeze([
10582
10722
  "notifier",
10583
10723
  "numeric-sensor",
10584
10724
  "oauth-integration",
10585
- "oidc-connections",
10586
10725
  "osd",
10587
10726
  "osd-manager",
10588
10727
  "pet-feeder",
@@ -12289,13 +12428,25 @@ Object.freeze({
12289
12428
  "path": "temperature",
12290
12429
  "valueType": "number"
12291
12430
  }],
12292
- "timed-run": [{
12293
- "path": "activeUntil",
12294
- "valueType": "number"
12295
- }, {
12296
- "path": "requestedBy",
12297
- "valueType": "string"
12298
- }],
12431
+ "timed-run": [
12432
+ {
12433
+ "path": "activeUntil",
12434
+ "valueType": "number"
12435
+ },
12436
+ {
12437
+ "path": "requestedBy",
12438
+ "valueType": "string"
12439
+ },
12440
+ {
12441
+ "path": "quickRun.duration",
12442
+ "valueType": "number"
12443
+ },
12444
+ {
12445
+ "path": "quickRun.unit",
12446
+ "valueType": "enum",
12447
+ "enumValues": ["s", "min"]
12448
+ }
12449
+ ],
12299
12450
  "update": [
12300
12451
  {
12301
12452
  "path": "currentVersion",
@@ -12474,8 +12625,7 @@ Object.freeze({
12474
12625
  "device-provider": { "reloadDevice": 18e4 },
12475
12626
  "llm": {
12476
12627
  "generate": 6e5,
12477
- "generateVision": 6e5,
12478
- "testProfile": 6e5
12628
+ "generateVision": 6e5
12479
12629
  },
12480
12630
  "llm-runtime": {
12481
12631
  "complete": 6e5,
@@ -12534,7 +12684,7 @@ function userError(code, params, englishFallback) {
12534
12684
  return new CamstackError(code, params, englishFallback);
12535
12685
  }
12536
12686
  //#endregion
12537
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12687
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12538
12688
  /**
12539
12689
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12540
12690
  *
@@ -13696,6 +13846,387 @@ var NodeColorIdSchema = _enum([
13696
13846
  "node-h"
13697
13847
  ]);
13698
13848
  //#endregion
13849
+ //#region ../types/dist/convert-BAzH15HU.mjs
13850
+ /**
13851
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13852
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13853
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13854
+ * decimals). K derives from `°C = K − 273.15`.
13855
+ */
13856
+ var UNIT_TABLE = {
13857
+ "°C": {
13858
+ dimension: "temperature",
13859
+ factor: 1,
13860
+ offset: 0
13861
+ },
13862
+ "°F": {
13863
+ dimension: "temperature",
13864
+ factor: 5 / 9,
13865
+ offset: -160 / 9
13866
+ },
13867
+ K: {
13868
+ dimension: "temperature",
13869
+ factor: 1,
13870
+ offset: -273.15
13871
+ },
13872
+ hPa: {
13873
+ dimension: "pressure",
13874
+ factor: 1,
13875
+ offset: 0
13876
+ },
13877
+ kPa: {
13878
+ dimension: "pressure",
13879
+ factor: 10,
13880
+ offset: 0
13881
+ },
13882
+ Pa: {
13883
+ dimension: "pressure",
13884
+ factor: .01,
13885
+ offset: 0
13886
+ },
13887
+ mbar: {
13888
+ dimension: "pressure",
13889
+ factor: 1,
13890
+ offset: 0
13891
+ },
13892
+ bar: {
13893
+ dimension: "pressure",
13894
+ factor: 1e3,
13895
+ offset: 0
13896
+ },
13897
+ inHg: {
13898
+ dimension: "pressure",
13899
+ factor: 33.8639,
13900
+ offset: 0
13901
+ },
13902
+ mmHg: {
13903
+ dimension: "pressure",
13904
+ factor: 1.33322,
13905
+ offset: 0
13906
+ },
13907
+ psi: {
13908
+ dimension: "pressure",
13909
+ factor: 68.9476,
13910
+ offset: 0
13911
+ },
13912
+ "m/s": {
13913
+ dimension: "speed",
13914
+ factor: 1,
13915
+ offset: 0
13916
+ },
13917
+ "km/h": {
13918
+ dimension: "speed",
13919
+ factor: 1 / 3.6,
13920
+ offset: 0
13921
+ },
13922
+ mph: {
13923
+ dimension: "speed",
13924
+ factor: .44704,
13925
+ offset: 0
13926
+ },
13927
+ kn: {
13928
+ dimension: "speed",
13929
+ factor: .514444,
13930
+ offset: 0
13931
+ },
13932
+ "mm/h": {
13933
+ dimension: "precipitation-rate",
13934
+ factor: 1,
13935
+ offset: 0
13936
+ },
13937
+ "in/h": {
13938
+ dimension: "precipitation-rate",
13939
+ factor: 25.4,
13940
+ offset: 0
13941
+ },
13942
+ m: {
13943
+ dimension: "length",
13944
+ factor: 1,
13945
+ offset: 0
13946
+ },
13947
+ mm: {
13948
+ dimension: "length",
13949
+ factor: .001,
13950
+ offset: 0
13951
+ },
13952
+ cm: {
13953
+ dimension: "length",
13954
+ factor: .01,
13955
+ offset: 0
13956
+ },
13957
+ km: {
13958
+ dimension: "length",
13959
+ factor: 1e3,
13960
+ offset: 0
13961
+ },
13962
+ in: {
13963
+ dimension: "length",
13964
+ factor: .0254,
13965
+ offset: 0
13966
+ },
13967
+ ft: {
13968
+ dimension: "length",
13969
+ factor: .3048,
13970
+ offset: 0
13971
+ },
13972
+ mi: {
13973
+ dimension: "length",
13974
+ factor: 1609.344,
13975
+ offset: 0
13976
+ },
13977
+ "m²": {
13978
+ dimension: "area",
13979
+ factor: 1,
13980
+ offset: 0
13981
+ },
13982
+ lx: {
13983
+ dimension: "illuminance",
13984
+ factor: 1,
13985
+ offset: 0
13986
+ },
13987
+ "W/m²": {
13988
+ dimension: "irradiance",
13989
+ factor: 1,
13990
+ offset: 0
13991
+ },
13992
+ W: {
13993
+ dimension: "power",
13994
+ factor: 1,
13995
+ offset: 0
13996
+ },
13997
+ kW: {
13998
+ dimension: "power",
13999
+ factor: 1e3,
14000
+ offset: 0
14001
+ },
14002
+ VA: {
14003
+ dimension: "apparent-power",
14004
+ factor: 1,
14005
+ offset: 0
14006
+ },
14007
+ Wh: {
14008
+ dimension: "energy",
14009
+ factor: 1,
14010
+ offset: 0
14011
+ },
14012
+ kWh: {
14013
+ dimension: "energy",
14014
+ factor: 1e3,
14015
+ offset: 0
14016
+ },
14017
+ MWh: {
14018
+ dimension: "energy",
14019
+ factor: 1e6,
14020
+ offset: 0
14021
+ },
14022
+ V: {
14023
+ dimension: "voltage",
14024
+ factor: 1,
14025
+ offset: 0
14026
+ },
14027
+ mV: {
14028
+ dimension: "voltage",
14029
+ factor: .001,
14030
+ offset: 0
14031
+ },
14032
+ A: {
14033
+ dimension: "current",
14034
+ factor: 1,
14035
+ offset: 0
14036
+ },
14037
+ mA: {
14038
+ dimension: "current",
14039
+ factor: .001,
14040
+ offset: 0
14041
+ },
14042
+ ppm: {
14043
+ dimension: "concentration-volume",
14044
+ factor: 1,
14045
+ offset: 0
14046
+ },
14047
+ ppb: {
14048
+ dimension: "concentration-volume",
14049
+ factor: .001,
14050
+ offset: 0
14051
+ },
14052
+ "µg/m³": {
14053
+ dimension: "concentration-mass",
14054
+ factor: 1,
14055
+ offset: 0
14056
+ },
14057
+ "mg/m³": {
14058
+ dimension: "concentration-mass",
14059
+ factor: 1e3,
14060
+ offset: 0
14061
+ },
14062
+ kg: {
14063
+ dimension: "mass",
14064
+ factor: 1,
14065
+ offset: 0
14066
+ },
14067
+ g: {
14068
+ dimension: "mass",
14069
+ factor: .001,
14070
+ offset: 0
14071
+ },
14072
+ lb: {
14073
+ dimension: "mass",
14074
+ factor: .453592,
14075
+ offset: 0
14076
+ },
14077
+ oz: {
14078
+ dimension: "mass",
14079
+ factor: .0283495,
14080
+ offset: 0
14081
+ },
14082
+ "%": {
14083
+ dimension: "percentage",
14084
+ factor: 1,
14085
+ offset: 0
14086
+ },
14087
+ L: {
14088
+ dimension: "volume",
14089
+ factor: 1,
14090
+ offset: 0
14091
+ },
14092
+ mL: {
14093
+ dimension: "volume",
14094
+ factor: .001,
14095
+ offset: 0
14096
+ },
14097
+ gal: {
14098
+ dimension: "volume",
14099
+ factor: 3.785411784,
14100
+ offset: 0
14101
+ },
14102
+ s: {
14103
+ dimension: "duration",
14104
+ factor: 1,
14105
+ offset: 0
14106
+ },
14107
+ ms: {
14108
+ dimension: "duration",
14109
+ factor: .001,
14110
+ offset: 0
14111
+ },
14112
+ min: {
14113
+ dimension: "duration",
14114
+ factor: 60,
14115
+ offset: 0
14116
+ },
14117
+ h: {
14118
+ dimension: "duration",
14119
+ factor: 3600,
14120
+ offset: 0
14121
+ },
14122
+ d: {
14123
+ dimension: "duration",
14124
+ factor: 86400,
14125
+ offset: 0
14126
+ },
14127
+ B: {
14128
+ dimension: "information",
14129
+ factor: 1,
14130
+ offset: 0
14131
+ },
14132
+ KiB: {
14133
+ dimension: "information",
14134
+ factor: 1024,
14135
+ offset: 0
14136
+ },
14137
+ MiB: {
14138
+ dimension: "information",
14139
+ factor: 1024 ** 2,
14140
+ offset: 0
14141
+ },
14142
+ GiB: {
14143
+ dimension: "information",
14144
+ factor: 1024 ** 3,
14145
+ offset: 0
14146
+ },
14147
+ TiB: {
14148
+ dimension: "information",
14149
+ factor: 1024 ** 4,
14150
+ offset: 0
14151
+ },
14152
+ kB: {
14153
+ dimension: "information",
14154
+ factor: 1e3,
14155
+ offset: 0
14156
+ },
14157
+ MB: {
14158
+ dimension: "information",
14159
+ factor: 1e6,
14160
+ offset: 0
14161
+ },
14162
+ GB: {
14163
+ dimension: "information",
14164
+ factor: 1e9,
14165
+ offset: 0
14166
+ },
14167
+ "°": {
14168
+ dimension: "angle",
14169
+ factor: 1,
14170
+ offset: 0
14171
+ },
14172
+ dBFS: {
14173
+ dimension: "sound-level",
14174
+ factor: 1,
14175
+ offset: 0
14176
+ },
14177
+ dBm: {
14178
+ dimension: "signal-level",
14179
+ factor: 1,
14180
+ offset: 0
14181
+ },
14182
+ mired: {
14183
+ dimension: "colour-temperature",
14184
+ factor: 1,
14185
+ offset: 0
14186
+ },
14187
+ kbps: {
14188
+ dimension: "bitrate",
14189
+ factor: 1,
14190
+ offset: 0
14191
+ },
14192
+ Mbps: {
14193
+ dimension: "bitrate",
14194
+ factor: 1e3,
14195
+ offset: 0
14196
+ },
14197
+ fps: {
14198
+ dimension: "rate",
14199
+ factor: 1,
14200
+ offset: 0
14201
+ }
14202
+ };
14203
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14204
+ function specFor(unit) {
14205
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14206
+ }
14207
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14208
+ function toCanonical(value, spec) {
14209
+ return value * spec.factor + spec.offset;
14210
+ }
14211
+ /** Canonical-unit value → the unit described by `spec`. */
14212
+ function fromCanonical(canonical, spec) {
14213
+ return (canonical - spec.offset) / spec.factor;
14214
+ }
14215
+ /**
14216
+ * Graceful variant for the render path: returns the converted value, or
14217
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14218
+ * Never throws — a render caller falls back to the raw value + source unit.
14219
+ */
14220
+ function tryConvertUnit(value, from, to) {
14221
+ if (from === to) return value;
14222
+ const fromSpec = specFor(from);
14223
+ if (!fromSpec) return void 0;
14224
+ const toSpec = specFor(to);
14225
+ if (!toSpec) return void 0;
14226
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14227
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14228
+ }
14229
+ //#endregion
13699
14230
  //#region ../types/dist/index.mjs
13700
14231
  var SMTP_INBOUND_DEFAULT_PORT = 2525;
13701
14232
  var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
@@ -14100,17 +14631,18 @@ function composeInstanceKinds(parts, base) {
14100
14631
  * message to what the kind supports — callers never special-case a service.
14101
14632
  *
14102
14633
  * DESIGN DECISIONS (locked):
14103
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
14104
- * `setTargetEnabled`), each provider persisting via the `settings-store`
14105
- * cap. Rationale: the admin UI needs one uniform surface across the
14106
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
14107
- * alternative would fork the UI per addon and cannot host the
14108
- * discovery→adopt flow.
14109
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
14110
- * the generated cap-mount auto-`concatCollection`-fans them across every
14111
- * registered provider (notifiers addon + HA addon) so one catalog is
14112
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
14113
- * `addonId` the generated collection router extracts from the call input.
14634
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14635
+ * providers serve their targets as `integration-instances` records, one
14636
+ * record kind per target kind, and the admin lists, adds, edits, switches
14637
+ * and deletes them through that cap. This cap keeps what an instance cannot
14638
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14639
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14640
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14641
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14642
+ * cap-mount auto-`concatCollection`-fans them across every registered
14643
+ * provider so one catalog is routable. `send` / `testTarget` /
14644
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14645
+ * collection router extracts from the call input.
14114
14646
  * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14115
14647
  * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14116
14648
  * `storage` / `storage-provider` / `recording` caps over the same path. No
@@ -14355,7 +14887,7 @@ var TargetKindCapsSchema = object({
14355
14887
  * the union is large and not meant for runtime validation here; the exported
14356
14888
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14357
14889
  */
14358
- var ConfigSchemaPassthrough$4 = unknown();
14890
+ var ConfigSchemaPassthrough$3 = unknown();
14359
14891
  var TargetKindSchema = object({
14360
14892
  kind: string(),
14361
14893
  label: string(),
@@ -14392,7 +14924,7 @@ var TargetKindSchema = object({
14392
14924
  * its raster path, which is the safe default for an unknown image.
14393
14925
  */
14394
14926
  iconMediaType: string().optional(),
14395
- configSchema: ConfigSchemaPassthrough$4,
14927
+ configSchema: ConfigSchemaPassthrough$3,
14396
14928
  /**
14397
14929
  * The config keys that decide WHERE this kind's secrets are sent (server,
14398
14930
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14414,9 +14946,11 @@ var TargetKindSchema = object({
14414
14946
  caps: TargetKindCapsSchema
14415
14947
  });
14416
14948
  /**
14417
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14418
- * (return a presence marker only) when serving `listTargets` — never
14419
- * round-trip a stored secret to the UI.
14949
+ * A target as a provider's own code sees it (the notification-center bridge, the
14950
+ * send path): the flat record body split into the name, the switch and the
14951
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14952
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14953
+ * secret to the UI.
14420
14954
  */
14421
14955
  var TargetSchema = object({
14422
14956
  id: string(),
@@ -14459,13 +14993,13 @@ var ProbeTargetConfigResultSchema = object({
14459
14993
  stateInstance: string().optional(),
14460
14994
  error: string().optional()
14461
14995
  });
14462
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14996
+ method(object({}), array(TargetKindSchema)), method(object({
14463
14997
  kind: string(),
14464
14998
  config: record(string(), unknown()).optional(),
14465
14999
  /**
14466
15000
  * When editing / duplicating, the blank password field never round-trips
14467
15001
  * the stored token. Pass the source target id so the provider can restore
14468
- * secrets server-side before dialling (same merge as upsert).
15002
+ * secrets server-side before dialling.
14469
15003
  */
14470
15004
  targetId: string().optional()
14471
15005
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14488,27 +15022,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14488
15022
  }), TestResultSchema, {
14489
15023
  kind: "mutation",
14490
15024
  auth: "admin"
14491
- }), method(object({
14492
- target: TargetSchema,
14493
- /**
14494
- * Duplicate flow: create a NEW target id while copying secrets from an
14495
- * existing row. The UI never holds the stored token; blank / redacted
14496
- * password fields are restored from this source (D634 still applies if
14497
- * destinations diverge without a re-typed secret).
14498
- */
14499
- cloneSecretsFrom: string().optional()
14500
- }), TargetSchema, {
14501
- kind: "mutation",
14502
- auth: "admin"
14503
- }), method(object({ targetId: string() }), _void(), {
14504
- kind: "mutation",
14505
- auth: "admin"
14506
- }), method(object({
14507
- targetId: string(),
14508
- enabled: boolean()
14509
- }), _void(), {
14510
- kind: "mutation",
14511
- auth: "admin"
14512
15025
  });
14513
15026
  looseObject({
14514
15027
  name: string().min(1),
@@ -22776,7 +23289,6 @@ var SYSTEM_CAP_VOCABULARY = {
22776
23289
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22777
23290
  "notification-rules": system("notification rules — authored on their own page"),
22778
23291
  "oauth-integration": system(IDENTITY),
22779
- "oidc-connections": system(IDENTITY),
22780
23292
  "osd-manager": system(PIPELINE),
22781
23293
  "pipeline-executor": system(PIPELINE),
22782
23294
  "pipeline-orchestrator": system(PIPELINE),
@@ -42797,6 +43309,7 @@ var ExpressionBindingSourceSchema = union([
42797
43309
  object({
42798
43310
  kind: literal("global"),
42799
43311
  sourceStableId: string(),
43312
+ addonId: string().optional(),
42800
43313
  cap: string(),
42801
43314
  fieldPath: string()
42802
43315
  })
@@ -42863,385 +43376,6 @@ var SourceInfoSchema = object({
42863
43376
  raw: record(string(), unknown()).optional()
42864
43377
  });
42865
43378
  /**
42866
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42867
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42868
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42869
- * decimals). K derives from `°C = K − 273.15`.
42870
- */
42871
- var UNIT_TABLE = {
42872
- "°C": {
42873
- dimension: "temperature",
42874
- factor: 1,
42875
- offset: 0
42876
- },
42877
- "°F": {
42878
- dimension: "temperature",
42879
- factor: 5 / 9,
42880
- offset: -160 / 9
42881
- },
42882
- K: {
42883
- dimension: "temperature",
42884
- factor: 1,
42885
- offset: -273.15
42886
- },
42887
- hPa: {
42888
- dimension: "pressure",
42889
- factor: 1,
42890
- offset: 0
42891
- },
42892
- kPa: {
42893
- dimension: "pressure",
42894
- factor: 10,
42895
- offset: 0
42896
- },
42897
- Pa: {
42898
- dimension: "pressure",
42899
- factor: .01,
42900
- offset: 0
42901
- },
42902
- mbar: {
42903
- dimension: "pressure",
42904
- factor: 1,
42905
- offset: 0
42906
- },
42907
- bar: {
42908
- dimension: "pressure",
42909
- factor: 1e3,
42910
- offset: 0
42911
- },
42912
- inHg: {
42913
- dimension: "pressure",
42914
- factor: 33.8639,
42915
- offset: 0
42916
- },
42917
- mmHg: {
42918
- dimension: "pressure",
42919
- factor: 1.33322,
42920
- offset: 0
42921
- },
42922
- psi: {
42923
- dimension: "pressure",
42924
- factor: 68.9476,
42925
- offset: 0
42926
- },
42927
- "m/s": {
42928
- dimension: "speed",
42929
- factor: 1,
42930
- offset: 0
42931
- },
42932
- "km/h": {
42933
- dimension: "speed",
42934
- factor: 1 / 3.6,
42935
- offset: 0
42936
- },
42937
- mph: {
42938
- dimension: "speed",
42939
- factor: .44704,
42940
- offset: 0
42941
- },
42942
- kn: {
42943
- dimension: "speed",
42944
- factor: .514444,
42945
- offset: 0
42946
- },
42947
- "mm/h": {
42948
- dimension: "precipitation-rate",
42949
- factor: 1,
42950
- offset: 0
42951
- },
42952
- "in/h": {
42953
- dimension: "precipitation-rate",
42954
- factor: 25.4,
42955
- offset: 0
42956
- },
42957
- m: {
42958
- dimension: "length",
42959
- factor: 1,
42960
- offset: 0
42961
- },
42962
- mm: {
42963
- dimension: "length",
42964
- factor: .001,
42965
- offset: 0
42966
- },
42967
- cm: {
42968
- dimension: "length",
42969
- factor: .01,
42970
- offset: 0
42971
- },
42972
- km: {
42973
- dimension: "length",
42974
- factor: 1e3,
42975
- offset: 0
42976
- },
42977
- in: {
42978
- dimension: "length",
42979
- factor: .0254,
42980
- offset: 0
42981
- },
42982
- ft: {
42983
- dimension: "length",
42984
- factor: .3048,
42985
- offset: 0
42986
- },
42987
- mi: {
42988
- dimension: "length",
42989
- factor: 1609.344,
42990
- offset: 0
42991
- },
42992
- "m²": {
42993
- dimension: "area",
42994
- factor: 1,
42995
- offset: 0
42996
- },
42997
- lx: {
42998
- dimension: "illuminance",
42999
- factor: 1,
43000
- offset: 0
43001
- },
43002
- "W/m²": {
43003
- dimension: "irradiance",
43004
- factor: 1,
43005
- offset: 0
43006
- },
43007
- W: {
43008
- dimension: "power",
43009
- factor: 1,
43010
- offset: 0
43011
- },
43012
- kW: {
43013
- dimension: "power",
43014
- factor: 1e3,
43015
- offset: 0
43016
- },
43017
- VA: {
43018
- dimension: "apparent-power",
43019
- factor: 1,
43020
- offset: 0
43021
- },
43022
- Wh: {
43023
- dimension: "energy",
43024
- factor: 1,
43025
- offset: 0
43026
- },
43027
- kWh: {
43028
- dimension: "energy",
43029
- factor: 1e3,
43030
- offset: 0
43031
- },
43032
- MWh: {
43033
- dimension: "energy",
43034
- factor: 1e6,
43035
- offset: 0
43036
- },
43037
- V: {
43038
- dimension: "voltage",
43039
- factor: 1,
43040
- offset: 0
43041
- },
43042
- mV: {
43043
- dimension: "voltage",
43044
- factor: .001,
43045
- offset: 0
43046
- },
43047
- A: {
43048
- dimension: "current",
43049
- factor: 1,
43050
- offset: 0
43051
- },
43052
- mA: {
43053
- dimension: "current",
43054
- factor: .001,
43055
- offset: 0
43056
- },
43057
- ppm: {
43058
- dimension: "concentration-volume",
43059
- factor: 1,
43060
- offset: 0
43061
- },
43062
- ppb: {
43063
- dimension: "concentration-volume",
43064
- factor: .001,
43065
- offset: 0
43066
- },
43067
- "µg/m³": {
43068
- dimension: "concentration-mass",
43069
- factor: 1,
43070
- offset: 0
43071
- },
43072
- "mg/m³": {
43073
- dimension: "concentration-mass",
43074
- factor: 1e3,
43075
- offset: 0
43076
- },
43077
- kg: {
43078
- dimension: "mass",
43079
- factor: 1,
43080
- offset: 0
43081
- },
43082
- g: {
43083
- dimension: "mass",
43084
- factor: .001,
43085
- offset: 0
43086
- },
43087
- lb: {
43088
- dimension: "mass",
43089
- factor: .453592,
43090
- offset: 0
43091
- },
43092
- oz: {
43093
- dimension: "mass",
43094
- factor: .0283495,
43095
- offset: 0
43096
- },
43097
- "%": {
43098
- dimension: "percentage",
43099
- factor: 1,
43100
- offset: 0
43101
- },
43102
- L: {
43103
- dimension: "volume",
43104
- factor: 1,
43105
- offset: 0
43106
- },
43107
- mL: {
43108
- dimension: "volume",
43109
- factor: .001,
43110
- offset: 0
43111
- },
43112
- gal: {
43113
- dimension: "volume",
43114
- factor: 3.785411784,
43115
- offset: 0
43116
- },
43117
- s: {
43118
- dimension: "duration",
43119
- factor: 1,
43120
- offset: 0
43121
- },
43122
- ms: {
43123
- dimension: "duration",
43124
- factor: .001,
43125
- offset: 0
43126
- },
43127
- min: {
43128
- dimension: "duration",
43129
- factor: 60,
43130
- offset: 0
43131
- },
43132
- h: {
43133
- dimension: "duration",
43134
- factor: 3600,
43135
- offset: 0
43136
- },
43137
- d: {
43138
- dimension: "duration",
43139
- factor: 86400,
43140
- offset: 0
43141
- },
43142
- B: {
43143
- dimension: "information",
43144
- factor: 1,
43145
- offset: 0
43146
- },
43147
- KiB: {
43148
- dimension: "information",
43149
- factor: 1024,
43150
- offset: 0
43151
- },
43152
- MiB: {
43153
- dimension: "information",
43154
- factor: 1024 ** 2,
43155
- offset: 0
43156
- },
43157
- GiB: {
43158
- dimension: "information",
43159
- factor: 1024 ** 3,
43160
- offset: 0
43161
- },
43162
- TiB: {
43163
- dimension: "information",
43164
- factor: 1024 ** 4,
43165
- offset: 0
43166
- },
43167
- kB: {
43168
- dimension: "information",
43169
- factor: 1e3,
43170
- offset: 0
43171
- },
43172
- MB: {
43173
- dimension: "information",
43174
- factor: 1e6,
43175
- offset: 0
43176
- },
43177
- GB: {
43178
- dimension: "information",
43179
- factor: 1e9,
43180
- offset: 0
43181
- },
43182
- "°": {
43183
- dimension: "angle",
43184
- factor: 1,
43185
- offset: 0
43186
- },
43187
- dBFS: {
43188
- dimension: "sound-level",
43189
- factor: 1,
43190
- offset: 0
43191
- },
43192
- dBm: {
43193
- dimension: "signal-level",
43194
- factor: 1,
43195
- offset: 0
43196
- },
43197
- mired: {
43198
- dimension: "colour-temperature",
43199
- factor: 1,
43200
- offset: 0
43201
- },
43202
- kbps: {
43203
- dimension: "bitrate",
43204
- factor: 1,
43205
- offset: 0
43206
- },
43207
- Mbps: {
43208
- dimension: "bitrate",
43209
- factor: 1e3,
43210
- offset: 0
43211
- },
43212
- fps: {
43213
- dimension: "rate",
43214
- factor: 1,
43215
- offset: 0
43216
- }
43217
- };
43218
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43219
- function specFor(unit) {
43220
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43221
- }
43222
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43223
- function toCanonical(value, spec) {
43224
- return value * spec.factor + spec.offset;
43225
- }
43226
- /** Canonical-unit value → the unit described by `spec`. */
43227
- function fromCanonical(canonical, spec) {
43228
- return (canonical - spec.offset) / spec.factor;
43229
- }
43230
- /**
43231
- * Graceful variant for the render path: returns the converted value, or
43232
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43233
- * Never throws — a render caller falls back to the raw value + source unit.
43234
- */
43235
- function tryConvertUnit(value, from, to) {
43236
- if (from === to) return value;
43237
- const fromSpec = specFor(from);
43238
- if (!fromSpec) return void 0;
43239
- const toSpec = specFor(to);
43240
- if (!toSpec) return void 0;
43241
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43242
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43243
- }
43244
- /**
43245
43379
  * A number a person types for a physical quantity — the input side of the
43246
43380
  * value + unit contract (D972).
43247
43381
  *
@@ -44937,15 +45071,28 @@ var SettingsSchemaWithValuesSchema = object({
44937
45071
  });
44938
45072
  /** Patch object — keys are field names, values are the new field values. */
44939
45073
  var SettingsPatchSchema = record(string(), unknown());
45074
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45075
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
45076
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
45077
+ var ScopedSettingsDeclarationSchema = object({
45078
+ cap: string(),
45079
+ scope: ScopedSettingsScopeSchema,
45080
+ sectionIds: array(string())
45081
+ });
44940
45082
  /** Standard success response for update operations. */
44941
45083
  var SettingsUpdateResultSchema = object({ success: literal(true) });
44942
45084
  method(object({
44943
45085
  addonId: string(),
44944
- nodeId: string().optional(),
44945
- overlay: record(string(), unknown()).optional(),
44946
- cap: string().optional()
45086
+ cap: string(),
45087
+ scope: ScopedSettingsScopeSchema,
45088
+ nodeId: string().optional()
44947
45089
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44948
45090
  addonId: string(),
45091
+ nodeId: string().optional()
45092
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
45093
+ addonId: string(),
45094
+ cap: string(),
45095
+ scope: ScopedSettingsScopeSchema,
44949
45096
  nodeId: string().optional(),
44950
45097
  patch: SettingsPatchSchema
44951
45098
  }), SettingsUpdateResultSchema, {
@@ -44953,8 +45100,10 @@ method(object({
44953
45100
  auth: "admin"
44954
45101
  }), method(object({
44955
45102
  addonId: string(),
45103
+ cap: string(),
45104
+ scope: ScopedSettingsScopeSchema,
44956
45105
  nodeId: string().optional()
44957
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45106
+ }), record(string(), unknown()).nullable()), method(object({
44958
45107
  addonId: string(),
44959
45108
  nodeId: string().optional(),
44960
45109
  instance: IntegrationInstanceRefSchema
@@ -46294,7 +46443,7 @@ var BackupDestinationInfoSchema = object({
46294
46443
  id: string(),
46295
46444
  /**
46296
46445
  * Manifest id of the owning addon. The Settings button on the
46297
- * destination card opens that addon's `globalSettingsSchema` panel.
46446
+ * destination card opens that addon's scoped settings panel.
46298
46447
  */
46299
46448
  addonId: string(),
46300
46449
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -48903,7 +49052,7 @@ DeviceType.MediaPlayer, method(deviceOnly, _void(), {
48903
49052
  kind: "mutation",
48904
49053
  auth: "protected"
48905
49054
  });
48906
- 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;
49055
+ 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;
48907
49056
  var NUMBER = {
48908
49057
  kind: "fixed",
48909
49058
  result: ["number"]
@@ -50774,6 +50923,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
50774
50923
  kind: "mutation",
50775
50924
  auth: "admin"
50776
50925
  });
50926
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
50777
50927
  /**
50778
50928
  * A SITE ZONE — a named part of the property that several locations belong to.
50779
50929
  *
@@ -51167,6 +51317,12 @@ var DeviceInfoSchema = object({
51167
51317
  addonId: string(),
51168
51318
  type: _enum(DeviceType),
51169
51319
  name: string(),
51320
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
51321
+ * entity_id style. Generated once from the name at first registration, unique
51322
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
51323
+ * Optional: a record produced by an older build omits it until the boot
51324
+ * backfill has run. */
51325
+ alias: DeviceAliasSchema.optional(),
51170
51326
  /** Operator-organisational location label. `null` when unset. */
51171
51327
  location: string().nullable(),
51172
51328
  /** Soft-disabled flag. */
@@ -51486,6 +51642,25 @@ method(object({
51486
51642
  }), _void(), {
51487
51643
  kind: "mutation",
51488
51644
  auth: "admin"
51645
+ }), method(object({
51646
+ deviceId: number(),
51647
+ alias: string()
51648
+ }), discriminatedUnion("ok", [object({
51649
+ ok: literal(true),
51650
+ alias: DeviceAliasSchema
51651
+ }), object({
51652
+ ok: literal(false),
51653
+ reason: _enum([
51654
+ "empty",
51655
+ "too-long",
51656
+ "grammar",
51657
+ "reserved",
51658
+ "taken"
51659
+ ]),
51660
+ takenBy: number().optional()
51661
+ })]), {
51662
+ kind: "mutation",
51663
+ auth: "admin"
51489
51664
  }), method(object({
51490
51665
  deviceId: number(),
51491
51666
  location: string().nullable()
@@ -54511,7 +54686,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54511
54686
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
54512
54687
  * clears it.
54513
54688
  */
54514
- var ConfigSchemaPassthrough$3 = custom();
54689
+ var ConfigSchemaPassthrough$2 = custom();
54515
54690
  var IntegrationInstanceIdSchema = string().min(1);
54516
54691
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54517
54692
  /**
@@ -54557,7 +54732,16 @@ var IntegrationInstanceKindSchema = object({
54557
54732
  /** Relative hub path to start an interactive account link of an instance of this kind. */
54558
54733
  connectPath: string().min(1).optional(),
54559
54734
  /** Relative hub path to disconnect that account. */
54560
- disconnectPath: string().min(1).optional()
54735
+ disconnectPath: string().min(1).optional(),
54736
+ /**
54737
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
54738
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
54739
+ * `connection` widgets write connection values, `model` widgets own the model choice.
54740
+ */
54741
+ widgets: array(object({
54742
+ id: string().min(1),
54743
+ placement: _enum(["connection", "model"])
54744
+ })).optional()
54561
54745
  });
54562
54746
  /**
54563
54747
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -54598,10 +54782,11 @@ var IntegrationInstanceDescriptionSchema = object({
54598
54782
  /** True when `testInstance` really probes the remote. */
54599
54783
  canTest: boolean(),
54600
54784
  /**
54601
- * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
54602
- * Add flow asks which first and sends it as `createInstance.kind`.
54785
+ * The kinds an instance can be of (D1013 rule 1), each naming its `functionCap`. One entry:
54786
+ * nothing to choose; more than one: the Add flow asks which first and sends it as
54787
+ * `createInstance.kind`. Never empty: every instance belongs to exactly one kind.
54603
54788
  */
54604
- kinds: array(IntegrationInstanceKindSchema).optional(),
54789
+ kinds: array(IntegrationInstanceKindSchema).min(1),
54605
54790
  /**
54606
54791
  * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
54607
54792
  * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
@@ -54637,8 +54822,8 @@ var IntegrationInstanceSchema = object({
54637
54822
  * the body is the one store, `setInstanceEnabled` the one writer (epic #440, step 3).
54638
54823
  */
54639
54824
  enabled: boolean().optional(),
54640
- /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
54641
- kind: string().optional(),
54825
+ /** The kind of this instance: exactly one of `describe().kinds[].id` (D1013 rule 2). */
54826
+ kind: string().min(1),
54642
54827
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
54643
54828
  formValues: record(string(), unknown()),
54644
54829
  status: IntegrationInstanceStatusSchema.optional(),
@@ -54662,7 +54847,7 @@ var integrationInstancesCapability = {
54662
54847
  id: IntegrationInstanceIdSchema.nullable(),
54663
54848
  /** The kind of the create form (`id: null`) of a provider with several. */
54664
54849
  kind: string().min(1).optional()
54665
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54850
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
54666
54851
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54667
54852
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54668
54853
  /**
@@ -55613,12 +55798,12 @@ var LlmModelInfoSchema = object({
55613
55798
  });
55614
55799
  /**
55615
55800
  * The provider-specific part of a profile, validated by the provider that owns
55616
- * the kind (`upsertProfile` refuses what it does not accept). A field its
55801
+ * the kind (saving an instance refuses what it does not accept). A field its
55617
55802
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
55618
55803
  * keychain, never in the row, and reads back as the redaction marker.
55619
55804
  */
55620
55805
  var LlmProviderConfigSchema = record(string(), unknown());
55621
- var LlmProfileSchema = object({
55806
+ object({
55622
55807
  id: string(),
55623
55808
  name: string(),
55624
55809
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -55712,8 +55897,8 @@ var LlmProviderWidgetSchema = object({
55712
55897
  /** ConfigUISchema tree passed through untyped on the wire (the
55713
55898
  * notification-output `ConfigSchemaPassthrough` precedent at
55714
55899
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
55715
- var ConfigSchemaPassthrough$2 = unknown();
55716
- var LlmProfileKindDescriptorSchema = object({
55900
+ var ConfigSchemaPassthrough$1 = unknown();
55901
+ object({
55717
55902
  kind: LlmProfileKindSchema,
55718
55903
  label: string(),
55719
55904
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -55721,9 +55906,9 @@ var LlmProfileKindDescriptorSchema = object({
55721
55906
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
55722
55907
  addonId: string(),
55723
55908
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
55724
- connectionSchema: ConfigSchemaPassthrough$2,
55909
+ connectionSchema: ConfigSchemaPassthrough$1,
55725
55910
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
55726
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
55911
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
55727
55912
  /** The kind's defaults; a model from `listModels` may override any of them. */
55728
55913
  features: LlmModelFeaturesSchema,
55729
55914
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -55852,20 +56037,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
55852
56037
  }), method(object({
55853
56038
  addonId: string().optional(),
55854
56039
  requestId: string()
55855
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
55856
- kind: "mutation",
55857
- auth: "admin"
55858
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
55859
- kind: "mutation",
55860
- auth: "admin"
55861
- }), method(ProfileRefInputSchema, _void(), {
55862
- kind: "mutation",
55863
- auth: "admin"
55864
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
55865
- kind: "mutation",
55866
- auth: "admin",
55867
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
55868
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
56040
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55869
56041
  kind: "mutation",
55870
56042
  auth: "admin"
55871
56043
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -56860,20 +57032,7 @@ method(_void(), MeshStatusSchema), method(object({
56860
57032
  /** Optional hostname override the host should advertise once joined. */
56861
57033
  hostname: string().optional() }), object({
56862
57034
  /** Authentication URL the operator should open in a browser. */
56863
- 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({
56864
- /** Optional auth key — when provided, probes the key validity
56865
- * against the provider's API. Omit when already joined to
56866
- * just ping the daemon. */
56867
- authKey: string().optional() }), object({
56868
- ok: boolean(),
56869
- /** Provider-side identifier resolved by the probe (tailnet
56870
- * name for Tailscale, network id for ZeroTier, etc.). */
56871
- tenant: string().optional(),
56872
- /** Daemon binary version, when reachable. */
56873
- daemonVersion: string().optional(),
56874
- /** Human-readable error when `ok: false`. */
56875
- error: string().optional()
56876
- }), { kind: "mutation" });
57035
+ 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() }));
56877
57036
  object({
56878
57037
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
56879
57038
  lastFetchedAt: number()
@@ -64234,6 +64393,11 @@ var NcScheduleSchema = object({
64234
64393
  * as a STATE SUMMARY at the end of each window of `schedule`.
64235
64394
  */
64236
64395
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
64396
+ _enum([
64397
+ "activation",
64398
+ "deactivation",
64399
+ "schedule"
64400
+ ]);
64237
64401
  /**
64238
64402
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
64239
64403
  * every section; `section` = one notification per section with something to
@@ -64250,11 +64414,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
64250
64414
  * midnight crossing); the send time is each window's END. Refused on the
64251
64415
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
64252
64416
  */
64253
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
64254
- mode: literal("summary"),
64255
- schedule: NcScheduleSchema,
64256
- split: NcStateSummarySplitSchema.optional()
64257
- })]);
64417
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
64418
+ object({ mode: literal("instant") }),
64419
+ object({
64420
+ mode: literal("summary"),
64421
+ schedule: NcScheduleSchema,
64422
+ split: NcStateSummarySplitSchema.optional()
64423
+ }),
64424
+ object({
64425
+ mode: literal("modes"),
64426
+ /** The bad edge, and every kind that has a single edge. */
64427
+ onActivation: boolean(),
64428
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
64429
+ onDeactivation: boolean(),
64430
+ /** A state summary at the end of each window; absent = no summary. */
64431
+ onSchedule: object({
64432
+ schedule: NcScheduleSchema,
64433
+ grouping: NcStateSummarySplitSchema
64434
+ }).optional()
64435
+ })
64436
+ ]);
64258
64437
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
64259
64438
  var NcPlateMatcherSchema = object({
64260
64439
  /**
@@ -66531,61 +66710,6 @@ var OauthIntegrationDescriptorSchema = object({
66531
66710
  });
66532
66711
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
66533
66712
  /**
66534
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
66535
- *
66536
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
66537
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
66538
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
66539
- * immutable after create.
66540
- */
66541
- var ConfigSchemaPassthrough$1 = unknown();
66542
- var OidcConnectionSchema = object({
66543
- id: string().min(1),
66544
- displayName: string(),
66545
- icon: string(),
66546
- issuerUrl: string(),
66547
- clientId: string(),
66548
- /** Client id + secret present — never the secret value. */
66549
- configured: boolean(),
66550
- /**
66551
- * Values for the connection form schema. `clientSecret` is a keychain
66552
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
66553
- */
66554
- formValues: record(string(), unknown()).optional()
66555
- });
66556
- /**
66557
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
66558
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
66559
- */
66560
- var OidcConnectionUpdateInputSchema = object({
66561
- id: string().min(1),
66562
- displayName: string().min(1),
66563
- icon: string().optional(),
66564
- issuerUrl: string().optional(),
66565
- clientId: string().optional(),
66566
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
66567
- redirectUri: string().optional(),
66568
- scopes: string().optional(),
66569
- usernameClaim: _enum([
66570
- "preferred_username",
66571
- "email",
66572
- "sub"
66573
- ]).optional()
66574
- });
66575
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
66576
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
66577
- displayName: string().min(1)
66578
- }), object({ id: string() }), {
66579
- kind: "mutation",
66580
- auth: "admin"
66581
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
66582
- kind: "mutation",
66583
- auth: "admin"
66584
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
66585
- kind: "mutation",
66586
- auth: "admin"
66587
- });
66588
- /**
66589
66713
  * Generic on-screen-display (video overlay) cap. Each camera exposes
66590
66714
  * a firmware-defined list of overlays (channel name, timestamp,
66591
66715
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -71636,6 +71760,7 @@ var RemoteLayoutComponentSchema = object({
71636
71760
  });
71637
71761
  });
71638
71762
  method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
71763
+ DeviceType.MediaPlayer;
71639
71764
  var RunningStateSourceSchema = _enum(["native", "assigned"]);
71640
71765
  object({
71641
71766
  /** null = unknown — never coerced to false (D393). */
@@ -72771,21 +72896,6 @@ method(object({
72771
72896
  */
72772
72897
  ttlSec: union([number().int().positive(), literal("never")]).optional()
72773
72898
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
72774
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
72775
- providerId: string().min(1),
72776
- displayName: string().min(1),
72777
- configSchema: unknown(),
72778
- nodeLocal: boolean(),
72779
- shouldSaveDiskSpace: literal(true),
72780
- minFreePercent: number().min(0).max(100)
72781
- }), object({
72782
- providerId: string().min(1),
72783
- displayName: string().min(1),
72784
- configSchema: unknown(),
72785
- nodeLocal: boolean(),
72786
- shouldSaveDiskSpace: literal(false),
72787
- minFreePercent: literal(null)
72788
- })]);
72789
72899
  method(object({
72790
72900
  location: StorageLocationRefSchema,
72791
72901
  relativePath: string()
@@ -72847,42 +72957,7 @@ method(object({
72847
72957
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
72848
72958
  kind: "mutation",
72849
72959
  auth: "admin"
72850
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
72851
- createdAt: true,
72852
- updatedAt: true
72853
- }), StorageLocationSchema, {
72854
- kind: "mutation",
72855
- auth: "admin"
72856
- }), method(object({
72857
- id: string(),
72858
- force: boolean().optional()
72859
- }), _void(), {
72860
- kind: "mutation",
72861
- auth: "admin"
72862
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
72863
- ok: boolean(),
72864
- error: string().optional()
72865
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
72866
- providerId: string(),
72867
- /**
72868
- * The location this config is an UNSAVED edit of, when there is one.
72869
- *
72870
- * `listLocations` replaces every declared secret with the redaction
72871
- * sentinel, so the edit modal's form state holds the sentinel for any
72872
- * credential the operator did not retype — and posting that here
72873
- * without a way to resolve it makes the provider try to authenticate
72874
- * as `__camstack_redacted__` and report the operator's own working
72875
- * password as wrong. Given this id, the orchestrator restores each
72876
- * sentinel from the stored config (same rule as `upsertLocation`)
72877
- * before dispatching. Omitted by the "Add location" wizard, where
72878
- * every value was typed just now and nothing is stored yet.
72879
- */
72880
- locationId: string().optional(),
72881
- config: record(string(), unknown())
72882
- }), object({
72883
- ok: boolean(),
72884
- error: string().optional()
72885
- }), { auth: "admin" });
72960
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
72886
72961
  /** One table of the settings database. */
72887
72962
  var StorageTableSchema = object({
72888
72963
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -74113,7 +74188,7 @@ var TerminalInstanceInfoSchema = object({
74113
74188
  /**
74114
74189
  * False while the node the instance is bound to is not in the cluster
74115
74190
  * topology (forgotten, renamed, re-paired). The instance is kept; the
74116
- * operator re-points it with `updateInstance({ targetNodeId })`.
74191
+ * operator re-points it on the Terminal page.
74117
74192
  */
74118
74193
  nodeInCluster: boolean(),
74119
74194
  profileId: string(),
@@ -74151,41 +74226,7 @@ var TerminalOutputBatchSchema = object({
74151
74226
  snapshot: string().optional(),
74152
74227
  events: array(TerminalOutputEventSchema).readonly()
74153
74228
  });
74154
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
74155
- targetNodeId: string().min(1),
74156
- profileId: string().min(1),
74157
- name: string().trim().min(1).max(160).optional(),
74158
- executable: string().max(1024).optional(),
74159
- args: array(string().max(2048)).max(64).optional(),
74160
- cwd: string().max(1024).optional(),
74161
- environment: array(string().max(4096)).max(64).optional(),
74162
- profileSettings: ProfileSettingsBagSchema.optional()
74163
- }), TerminalInstanceInfoSchema, {
74164
- kind: "mutation",
74165
- auth: "admin"
74166
- }), method(object({
74167
- instanceId: string().min(1),
74168
- /** Re-point the instance to another node; the profile must exist there. */
74169
- targetNodeId: string().min(1).max(256).optional(),
74170
- name: string().trim().min(1).max(160).optional(),
74171
- executable: string().max(1024).optional(),
74172
- args: array(string().max(2048)).max(64).optional(),
74173
- cwd: string().max(1024).optional(),
74174
- environment: array(string().max(4096)).max(64).optional(),
74175
- profileSettings: ProfileSettingsBagSchema.optional()
74176
- }), TerminalInstanceInfoSchema, {
74177
- kind: "mutation",
74178
- auth: "admin"
74179
- }), method(object({ instanceId: string().min(1) }), _void(), {
74180
- kind: "mutation",
74181
- auth: "admin"
74182
- }), method(object({
74183
- instanceId: string().min(1),
74184
- enabled: boolean()
74185
- }), TerminalInstanceInfoSchema, {
74186
- kind: "mutation",
74187
- auth: "admin"
74188
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74229
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74189
74230
  stableId: string().min(1),
74190
74231
  name: string().trim().min(1).max(160).optional()
74191
74232
  }), TerminalInstanceInfoSchema, {
@@ -74254,6 +74295,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
74254
74295
  _enum(["switch", "pet-fountain"]);
74255
74296
  DeviceType.Heater, DeviceType.PetFountain;
74256
74297
  DeviceType.Heater, DeviceType.PetFountain;
74298
+ /** One duration, as the one-tap run asks for it. */
74299
+ var TimedRunQuickRunSchema = object({
74300
+ duration: number().positive().finite(),
74301
+ unit: TimedRunUnitSchema
74302
+ });
74257
74303
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
74258
74304
  var TimedRunRefusalSchema = _enum([
74259
74305
  "unsupported-device",
@@ -74274,9 +74320,30 @@ object({
74274
74320
  activeUntil: number().nullable(),
74275
74321
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
74276
74322
  requestedBy: string().nullable(),
74323
+ /**
74324
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
74325
+ * the device is gone or its type has no timed run; a status from before this field
74326
+ * existed has none, and a surface falls back to the type's default.
74327
+ */
74328
+ quickRun: TimedRunQuickRunSchema.nullable(),
74277
74329
  lastFetchedAt: number()
74278
74330
  });
74279
- method(object({
74331
+ /** What the settings form needs: the unit, the bounds and the type's default. */
74332
+ var TimedRunOptionsSchema = object({
74333
+ unit: TimedRunUnitSchema,
74334
+ /** The longest run, in `unit`. */
74335
+ maxDuration: number(),
74336
+ defaultDuration: number()
74337
+ });
74338
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
74339
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
74340
+ method(object({ deviceId: number().int() }), TimedRunOptionsSchema), method(object({
74341
+ deviceId: number().int(),
74342
+ settings: TimedRunSettingsPatchSchema
74343
+ }), _void(), {
74344
+ kind: "mutation",
74345
+ auth: "admin"
74346
+ }), method(object({
74280
74347
  deviceId: number().int(),
74281
74348
  duration: number().positive().finite(),
74282
74349
  unit: TimedRunUnitSchema
@@ -76982,21 +77049,28 @@ Object.freeze({
76982
77049
  access: "view",
76983
77050
  role: "admin"
76984
77051
  },
76985
- "addonSettings.getGlobalSettings": {
77052
+ "addonSettings.getIntegrationFormSchema": {
77053
+ capName: "addon-settings",
77054
+ capScope: "system",
77055
+ addonId: null,
77056
+ access: "view",
77057
+ role: "admin"
77058
+ },
77059
+ "addonSettings.getScopedSettings": {
76986
77060
  capName: "addon-settings",
76987
77061
  capScope: "system",
76988
77062
  addonId: null,
76989
77063
  access: "view",
76990
77064
  role: "admin"
76991
77065
  },
76992
- "addonSettings.getIntegrationFormSchema": {
77066
+ "addonSettings.listScopedSettings": {
76993
77067
  capName: "addon-settings",
76994
77068
  capScope: "system",
76995
77069
  addonId: null,
76996
77070
  access: "view",
76997
77071
  role: "admin"
76998
77072
  },
76999
- "addonSettings.getIntegrationSettings": {
77073
+ "addonSettings.readScopedValues": {
77000
77074
  capName: "addon-settings",
77001
77075
  capScope: "system",
77002
77076
  addonId: null,
@@ -77017,7 +77091,7 @@ Object.freeze({
77017
77091
  access: "create",
77018
77092
  role: "admin"
77019
77093
  },
77020
- "addonSettings.updateGlobalSettings": {
77094
+ "addonSettings.updateScopedSettings": {
77021
77095
  capName: "addon-settings",
77022
77096
  capScope: "system",
77023
77097
  addonId: null,
@@ -79187,6 +79261,13 @@ Object.freeze({
79187
79261
  access: "create",
79188
79262
  role: "admin"
79189
79263
  },
79264
+ "deviceManager.setAlias": {
79265
+ capName: "device-manager",
79266
+ capScope: "system",
79267
+ addonId: null,
79268
+ access: "create",
79269
+ role: "admin"
79270
+ },
79190
79271
  "deviceManager.setChildLayout": {
79191
79272
  capName: "device-manager",
79192
79273
  capScope: "system",
@@ -80475,13 +80556,6 @@ Object.freeze({
80475
80556
  access: "delete",
80476
80557
  role: "admin"
80477
80558
  },
80478
- "llm.deleteProfile": {
80479
- capName: "llm",
80480
- capScope: "system",
80481
- addonId: null,
80482
- access: "delete",
80483
- role: "admin"
80484
- },
80485
80559
  "llm.generate": {
80486
80560
  capName: "llm",
80487
80561
  capScope: "system",
@@ -80545,20 +80619,6 @@ Object.freeze({
80545
80619
  access: "view",
80546
80620
  role: "admin"
80547
80621
  },
80548
- "llm.listProfileKinds": {
80549
- capName: "llm",
80550
- capScope: "system",
80551
- addonId: null,
80552
- access: "view",
80553
- role: "admin"
80554
- },
80555
- "llm.listProfiles": {
80556
- capName: "llm",
80557
- capScope: "system",
80558
- addonId: null,
80559
- access: "view",
80560
- role: "admin"
80561
- },
80562
80622
  "llm.listRuntimeNodes": {
80563
80623
  capName: "llm",
80564
80624
  capScope: "system",
@@ -80566,13 +80626,6 @@ Object.freeze({
80566
80626
  access: "view",
80567
80627
  role: "admin"
80568
80628
  },
80569
- "llm.migrateRawSecrets": {
80570
- capName: "llm",
80571
- capScope: "system",
80572
- addonId: null,
80573
- access: "create",
80574
- role: "admin"
80575
- },
80576
80629
  "llm.resolveModelRef": {
80577
80630
  capName: "llm",
80578
80631
  capScope: "system",
@@ -80601,20 +80654,6 @@ Object.freeze({
80601
80654
  access: "create",
80602
80655
  role: "admin"
80603
80656
  },
80604
- "llm.testProfile": {
80605
- capName: "llm",
80606
- capScope: "system",
80607
- addonId: null,
80608
- access: "create",
80609
- role: "admin"
80610
- },
80611
- "llm.upsertProfile": {
80612
- capName: "llm",
80613
- capScope: "system",
80614
- addonId: null,
80615
- access: "create",
80616
- role: "admin"
80617
- },
80618
80657
  "llmRuntime.appleAvailability": {
80619
80658
  capName: "llm-runtime",
80620
80659
  capScope: "system",
@@ -81119,13 +81158,6 @@ Object.freeze({
81119
81158
  access: "create",
81120
81159
  role: "admin"
81121
81160
  },
81122
- "meshNetwork.testConnection": {
81123
- capName: "mesh-network",
81124
- capScope: "system",
81125
- addonId: null,
81126
- access: "create",
81127
- role: "admin"
81128
- },
81129
81161
  "meter.getStatus": {
81130
81162
  capName: "meter",
81131
81163
  capScope: "device",
@@ -81784,13 +81816,6 @@ Object.freeze({
81784
81816
  access: "create",
81785
81817
  role: "admin"
81786
81818
  },
81787
- "notificationOutput.deleteTarget": {
81788
- capName: "notification-output",
81789
- capScope: "system",
81790
- addonId: null,
81791
- access: "delete",
81792
- role: "admin"
81793
- },
81794
81819
  "notificationOutput.discoverTargets": {
81795
81820
  capName: "notification-output",
81796
81821
  capScope: "system",
@@ -81798,13 +81823,6 @@ Object.freeze({
81798
81823
  access: "view",
81799
81824
  role: "admin"
81800
81825
  },
81801
- "notificationOutput.getTarget": {
81802
- capName: "notification-output",
81803
- capScope: "system",
81804
- addonId: null,
81805
- access: "view",
81806
- role: "admin"
81807
- },
81808
81826
  "notificationOutput.listTargetKinds": {
81809
81827
  capName: "notification-output",
81810
81828
  capScope: "system",
@@ -81812,13 +81830,6 @@ Object.freeze({
81812
81830
  access: "view",
81813
81831
  role: "admin"
81814
81832
  },
81815
- "notificationOutput.listTargets": {
81816
- capName: "notification-output",
81817
- capScope: "system",
81818
- addonId: null,
81819
- access: "view",
81820
- role: "admin"
81821
- },
81822
81833
  "notificationOutput.probeTargetConfig": {
81823
81834
  capName: "notification-output",
81824
81835
  capScope: "system",
@@ -81833,13 +81844,6 @@ Object.freeze({
81833
81844
  access: "create",
81834
81845
  role: "admin"
81835
81846
  },
81836
- "notificationOutput.setTargetEnabled": {
81837
- capName: "notification-output",
81838
- capScope: "system",
81839
- addonId: null,
81840
- access: "create",
81841
- role: "admin"
81842
- },
81843
81847
  "notificationOutput.testTarget": {
81844
81848
  capName: "notification-output",
81845
81849
  capScope: "system",
@@ -81847,13 +81851,6 @@ Object.freeze({
81847
81851
  access: "create",
81848
81852
  role: "admin"
81849
81853
  },
81850
- "notificationOutput.upsertTarget": {
81851
- capName: "notification-output",
81852
- capScope: "system",
81853
- addonId: null,
81854
- access: "create",
81855
- role: "admin"
81856
- },
81857
81854
  "notificationRules.activateAlarmProfile": {
81858
81855
  capName: "notification-rules",
81859
81856
  capScope: "system",
@@ -82113,41 +82110,6 @@ Object.freeze({
82113
82110
  access: "view",
82114
82111
  role: "admin"
82115
82112
  },
82116
- "oidcConnections.connectionFormSchema": {
82117
- capName: "oidc-connections",
82118
- capScope: "system",
82119
- addonId: null,
82120
- access: "view",
82121
- role: "admin"
82122
- },
82123
- "oidcConnections.createConnection": {
82124
- capName: "oidc-connections",
82125
- capScope: "system",
82126
- addonId: null,
82127
- access: "create",
82128
- role: "admin"
82129
- },
82130
- "oidcConnections.deleteConnection": {
82131
- capName: "oidc-connections",
82132
- capScope: "system",
82133
- addonId: null,
82134
- access: "delete",
82135
- role: "admin"
82136
- },
82137
- "oidcConnections.listConnections": {
82138
- capName: "oidc-connections",
82139
- capScope: "system",
82140
- addonId: null,
82141
- access: "view",
82142
- role: "admin"
82143
- },
82144
- "oidcConnections.updateConnection": {
82145
- capName: "oidc-connections",
82146
- capScope: "system",
82147
- addonId: null,
82148
- access: "create",
82149
- role: "admin"
82150
- },
82151
82113
  "osd.getStatus": {
82152
82114
  capName: "osd",
82153
82115
  capScope: "device",
@@ -85179,13 +85141,6 @@ Object.freeze({
85179
85141
  access: "delete",
85180
85142
  role: "admin"
85181
85143
  },
85182
- "storage.deleteLocation": {
85183
- capName: "storage",
85184
- capScope: "system",
85185
- addonId: null,
85186
- access: "delete",
85187
- role: "admin"
85188
- },
85189
85144
  "storage.endDownload": {
85190
85145
  capName: "storage",
85191
85146
  capScope: "system",
@@ -85242,13 +85197,6 @@ Object.freeze({
85242
85197
  access: "view",
85243
85198
  role: "admin"
85244
85199
  },
85245
- "storage.listProviders": {
85246
- capName: "storage",
85247
- capScope: "system",
85248
- addonId: null,
85249
- access: "view",
85250
- role: "admin"
85251
- },
85252
85200
  "storage.read": {
85253
85201
  capName: "storage",
85254
85202
  capScope: "system",
@@ -85270,27 +85218,6 @@ Object.freeze({
85270
85218
  access: "view",
85271
85219
  role: "admin"
85272
85220
  },
85273
- "storage.testConfig": {
85274
- capName: "storage",
85275
- capScope: "system",
85276
- addonId: null,
85277
- access: "view",
85278
- role: "admin"
85279
- },
85280
- "storage.testLocation": {
85281
- capName: "storage",
85282
- capScope: "system",
85283
- addonId: null,
85284
- access: "view",
85285
- role: "admin"
85286
- },
85287
- "storage.upsertLocation": {
85288
- capName: "storage",
85289
- capScope: "system",
85290
- addonId: null,
85291
- access: "create",
85292
- role: "admin"
85293
- },
85294
85221
  "storage.write": {
85295
85222
  capName: "storage",
85296
85223
  capScope: "system",
@@ -86068,27 +85995,6 @@ Object.freeze({
86068
85995
  access: "create",
86069
85996
  role: "admin"
86070
85997
  },
86071
- "terminalSession.createInstance": {
86072
- capName: "terminal-session",
86073
- capScope: "system",
86074
- addonId: null,
86075
- access: "create",
86076
- role: "admin"
86077
- },
86078
- "terminalSession.deleteInstance": {
86079
- capName: "terminal-session",
86080
- capScope: "system",
86081
- addonId: null,
86082
- access: "delete",
86083
- role: "admin"
86084
- },
86085
- "terminalSession.listInstances": {
86086
- capName: "terminal-session",
86087
- capScope: "system",
86088
- addonId: null,
86089
- access: "view",
86090
- role: "admin"
86091
- },
86092
85998
  "terminalSession.listLegacyCameras": {
86093
85999
  capName: "terminal-session",
86094
86000
  capScope: "system",
@@ -86131,20 +86037,6 @@ Object.freeze({
86131
86037
  access: "create",
86132
86038
  role: "admin"
86133
86039
  },
86134
- "terminalSession.setInstanceEnabled": {
86135
- capName: "terminal-session",
86136
- capScope: "system",
86137
- addonId: null,
86138
- access: "create",
86139
- role: "admin"
86140
- },
86141
- "terminalSession.updateInstance": {
86142
- capName: "terminal-session",
86143
- capScope: "system",
86144
- addonId: null,
86145
- access: "create",
86146
- role: "admin"
86147
- },
86148
86040
  "terminalSession.writeInput": {
86149
86041
  capName: "terminal-session",
86150
86042
  capScope: "system",
@@ -86159,6 +86051,13 @@ Object.freeze({
86159
86051
  access: "create",
86160
86052
  role: "viewer"
86161
86053
  },
86054
+ "timedRun.getOptions": {
86055
+ capName: "timed-run",
86056
+ capScope: "device",
86057
+ addonId: null,
86058
+ access: "view",
86059
+ role: "viewer"
86060
+ },
86162
86061
  "timedRun.getStatus": {
86163
86062
  capName: "timed-run",
86164
86063
  capScope: "device",
@@ -86173,6 +86072,13 @@ Object.freeze({
86173
86072
  access: "create",
86174
86073
  role: "viewer"
86175
86074
  },
86075
+ "timedRun.setSettings": {
86076
+ capName: "timed-run",
86077
+ capScope: "device",
86078
+ addonId: null,
86079
+ access: "create",
86080
+ role: "admin"
86081
+ },
86176
86082
  "toast.onToast": {
86177
86083
  capName: "toast",
86178
86084
  capScope: "system",
@@ -87103,6 +87009,7 @@ function offered(received) {
87103
87009
  };
87104
87010
  }
87105
87011
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
87012
+ string().min(1).max(4096);
87106
87013
  Object.freeze({
87107
87014
  "accessories.getStatus": [{
87108
87015
  name: "deviceId",
@@ -87786,6 +87693,11 @@ Object.freeze({
87786
87693
  form: "single",
87787
87694
  optional: false
87788
87695
  }],
87696
+ "deviceManager.setAlias": [{
87697
+ name: "deviceId",
87698
+ form: "single",
87699
+ optional: false
87700
+ }],
87789
87701
  "deviceManager.setChildLayout": [{
87790
87702
  name: "deviceId",
87791
87703
  form: "single",
@@ -89850,6 +89762,11 @@ Object.freeze({
89850
89762
  form: "single",
89851
89763
  optional: false
89852
89764
  }],
89765
+ "timedRun.getOptions": [{
89766
+ name: "deviceId",
89767
+ form: "single",
89768
+ optional: false
89769
+ }],
89853
89770
  "timedRun.getStatus": [{
89854
89771
  name: "deviceId",
89855
89772
  form: "single",
@@ -89860,6 +89777,11 @@ Object.freeze({
89860
89777
  form: "single",
89861
89778
  optional: false
89862
89779
  }],
89780
+ "timedRun.setSettings": [{
89781
+ name: "deviceId",
89782
+ form: "single",
89783
+ optional: false
89784
+ }],
89863
89785
  "update.getStatus": [{
89864
89786
  name: "deviceId",
89865
89787
  form: "single",
@@ -90722,6 +90644,7 @@ var STATE_SUMMARY_VARS = [
90722
90644
  [...RULE_TEXT],
90723
90645
  [...RULE_TEXT],
90724
90646
  [...RULE_TEXT],
90647
+ [...RULE_TEXT],
90725
90648
  [...RULE_BODY_ONLY],
90726
90649
  [...RULE_TEXT],
90727
90650
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
@@ -91843,222 +91766,6 @@ var GoogleBrokerRegistry = class {
91843
91766
  }
91844
91767
  };
91845
91768
  //#endregion
91846
- //#region src/google-settings-copy.ts
91847
- /**
91848
- * Operator-facing setup copy for Connectivity → Calendars / Email.
91849
- * Keep Connect as a same-origin relative link so the admin session cookie
91850
- * rides along (an absolute Public URL from a LAN admin tab drops the cookie).
91851
- * Root-relative hrefs must survive `sanitizeInfoHtml` in ui-library.
91852
- */
91853
- /** Enable APIs library in Google Cloud Console. */
91854
- var GOOGLE_CONSOLE_APIS_URL = "https://console.cloud.google.com/apis/library";
91855
- /** Create / manage OAuth Web clients. */
91856
- var GOOGLE_CONSOLE_CREDENTIALS_URL = "https://console.cloud.google.com/apis/credentials";
91857
- function redirectUriDisplay(publicOrigin) {
91858
- if (publicOrigin === null || publicOrigin.length === 0) return `<public-hub>${OAUTH_CALLBACK_SUFFIX}`;
91859
- return `${publicOrigin.replace(/\/$/, "")}${OAUTH_CALLBACK_SUFFIX}`;
91860
- }
91861
- function consoleLinksHtml$1() {
91862
- return [
91863
- `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
91864
- ` · `,
91865
- `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
91866
- ].join("");
91867
- }
91868
- function introHtml(cap, publicOrigin) {
91869
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
91870
- const console = consoleLinksHtml$1();
91871
- if (cap === "smtp-provider") return [
91872
- "<ol>",
91873
- `<li>Open Google Cloud Console (${console}) and enable the <strong>Gmail API</strong> (and Calendar API if you also use Calendars).</li>`,
91874
- "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
91875
- `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
91876
- "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
91877
- `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
91878
- "<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>",
91879
- "</ol>"
91880
- ].join("");
91881
- if (cap === "calendar-source") return [
91882
- "<ol>",
91883
- `<li>Open Google Cloud Console (${console}) and enable the <strong>Google Calendar API</strong> (and Gmail API if you also use Email).</li>`,
91884
- "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
91885
- `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
91886
- "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
91887
- `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
91888
- "<li>Optionally restrict which calendars are exposed with the filter below.</li>",
91889
- "</ol>"
91890
- ].join("");
91891
- return [
91892
- "<ol>",
91893
- `<li>Open Google Cloud Console (${console}) and enable <strong>Google Calendar API</strong> and <strong>Gmail API</strong>.</li>`,
91894
- "<li>Create a <strong>Web application</strong> OAuth client under Credentials.</li>",
91895
- `<li>Register redirect URI: <code>${redirect}</code> (Network → Public endpoint).</li>`,
91896
- `<li>Paste Client ID / secret, Save, then <a href="${OAUTH_START_PATH}">Connect Google</a>.</li>`,
91897
- "<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>",
91898
- "</ol>"
91899
- ].join("");
91900
- }
91901
- function connectHtml(cap, publicOrigin) {
91902
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
91903
- if (cap === "smtp-provider") return [
91904
- `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
91905
- "Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
91906
- `Google must list exactly: <code>${redirect}</code>. `,
91907
- "If an older Connect predates Gmail send, revoke CamStack in Google Account → Security and Connect again. ",
91908
- `Console: ${consoleLinksHtml$1()}.`
91909
- ].join("");
91910
- if (cap === "calendar-source") return [
91911
- `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
91912
- "Connect requests <code>calendar.readonly</code> (and Gmail send for the Email page). ",
91913
- `Google must list exactly: <code>${redirect}</code>. `,
91914
- `Console: ${consoleLinksHtml$1()}.`
91915
- ].join("");
91916
- return [
91917
- `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a>. `,
91918
- `Redirect URI registered in Google Cloud must be <code>${redirect}</code>. `,
91919
- `Console: ${consoleLinksHtml$1()}.`
91920
- ].join("");
91921
- }
91922
- function escapeHtml$1(s) {
91923
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
91924
- }
91925
- //#endregion
91926
- //#region src/google-broker-settings-schema.ts
91927
- /**
91928
- * Per-kind broker creation/edit form for kind=`google`.
91929
- * Account linking (Sign in) is driven by the kind's `connectPath`
91930
- * on the instances page — not a hard-coded Connect link in this form.
91931
- */
91932
- function consoleLinksHtml() {
91933
- return [
91934
- `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
91935
- ` · `,
91936
- `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
91937
- ].join("");
91938
- }
91939
- /** Setup steps shown on Add / Edit — includes Audience / Test users. */
91940
- function googleBrokerSetupHtml(publicOrigin) {
91941
- const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
91942
- return [
91943
- "<ol>",
91944
- `<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>`,
91945
- "<li>In that project, enable <strong>Google Calendar API</strong> and/or <strong>Gmail API</strong> (API library).</li>",
91946
- "<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>",
91947
- "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
91948
- `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
91949
- "<li>Paste Client ID and Client secret below. <strong>Test connection</strong> checks those with Google (not the account yet).</li>",
91950
- "<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>",
91951
- "</ol>"
91952
- ].join("");
91953
- }
91954
- function buildGoogleBrokerSettingsSchema(publicOrigin) {
91955
- return { sections: [
91956
- {
91957
- id: "setup-intro",
91958
- title: "Setup",
91959
- titleKey: "google-services:googleBrokerSettings.s_setup-intro.title",
91960
- description: "Google Cloud Console — register the hub redirect before Add / Sign in.",
91961
- descriptionKey: "google-services:googleBrokerSettings.s_setup-intro.description",
91962
- columns: 1,
91963
- fields: [{
91964
- type: "info",
91965
- key: "_setupSteps",
91966
- label: "Steps",
91967
- labelKey: "google-services:googleBrokerSettings.s_setup-intro._setupSteps.label",
91968
- format: "html",
91969
- content: googleBrokerSetupHtml(publicOrigin)
91970
- }]
91971
- },
91972
- {
91973
- id: "google-oauth-client",
91974
- title: "Google OAuth client",
91975
- titleKey: "google-services:googleBrokerSettings.s_google-oauth-client.title",
91976
- description: "Web application client from Google Cloud Console. The same Cloud project can be reused across Google brokers.",
91977
- descriptionKey: "google-services:googleBrokerSettings.s_google-oauth-client.description",
91978
- columns: 1,
91979
- fields: [{
91980
- type: "text",
91981
- key: "clientId",
91982
- label: "Client ID",
91983
- labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientId.label"
91984
- }, {
91985
- type: "password",
91986
- key: "clientSecret",
91987
- label: "Client secret",
91988
- labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientSecret.label",
91989
- showToggle: true
91990
- }]
91991
- },
91992
- {
91993
- id: "gmail-send",
91994
- title: "Gmail send",
91995
- titleKey: "google-services:googleBrokerSettings.s_gmail-send.title",
91996
- description: "From must be this connected account or a verified send-as. Disable Email on Connectivity to turn Gmail off without removing the broker.",
91997
- descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.description",
91998
- columns: 1,
91999
- fields: [{
92000
- type: "text",
92001
- key: "fromAddress",
92002
- label: "From address",
92003
- labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.label",
92004
- description: "Defaults to the connected Google account email when empty.",
92005
- descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.description"
92006
- }, {
92007
- type: "text",
92008
- key: "fromName",
92009
- label: "From display name (optional)",
92010
- labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromName.label"
92011
- }]
92012
- }
92013
- ] };
92014
- }
92015
- /** Connectivity Calendars / Email pages — the calendar import picker is on each broker. */
92016
- function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
92017
- const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
92018
- if (cap === "calendar-source") return { sections: [{
92019
- id: "setup-intro",
92020
- title: "Google brokers",
92021
- titleKey: "google-services:googleFeatureSettings.s_setup-intro.title",
92022
- columns: 1,
92023
- fields: [{
92024
- type: "info",
92025
- key: "_setupSteps",
92026
- label: "Setup",
92027
- labelKey: "google-services:googleFeatureSettings.s_setup-intro._setupSteps.label",
92028
- format: "html",
92029
- content: [
92030
- "<p>Link a Google account under Connectivity → <strong>Brokers</strong> ",
92031
- "(Client ID / secret + Sign in), then add a calendar under Connectivity → <strong>Calendars</strong>. ",
92032
- `OAuth redirect URI: <code>${redirect}</code>.</p>`
92033
- ].join("")
92034
- }]
92035
- }] };
92036
- return { sections: [{
92037
- id: "setup-intro",
92038
- title: cap === "smtp-provider" ? "Gmail send uses Google brokers" : "Google brokers",
92039
- columns: 1,
92040
- fields: [{
92041
- type: "info",
92042
- key: "_setupSteps",
92043
- label: "Setup",
92044
- labelKey: "google-services:googleFeatureSettings._setupSteps.label",
92045
- format: "html",
92046
- content: cap === "smtp-provider" ? [
92047
- "<p>Add or edit a <strong>Google</strong> broker under Connectivity → <strong>Brokers</strong> ",
92048
- "(Client ID / secret + Sign in). From address and send-as live on that broker. ",
92049
- `OAuth redirect URI: <code>${redirect}</code>.</p>`
92050
- ].join("") : [
92051
- "<p>Add or edit a <strong>Google</strong> broker under Connectivity → <strong>Brokers</strong> ",
92052
- "(Client ID / secret + Sign in). ",
92053
- `OAuth redirect URI: <code>${redirect}</code>.</p>`
92054
- ].join("")
92055
- }]
92056
- }] };
92057
- }
92058
- function escapeHtml(s) {
92059
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92060
- }
92061
- //#endregion
92062
91769
  //#region src/google-calendar-source.ts
92063
91770
  function parseGoogleInstant(value) {
92064
91771
  const ms = Date.parse(value);
@@ -92555,7 +92262,13 @@ function buildGoogleCalendarInstances(records, deps) {
92555
92262
  role: "services",
92556
92263
  instanceMode: "multiple",
92557
92264
  kind: GOOGLE_CALENDAR_INSTANCE_KIND,
92558
- canTest: false
92265
+ canTest: false,
92266
+ kinds: [{
92267
+ id: GOOGLE_CALENDAR_INSTANCE_KIND,
92268
+ label: "Google calendar",
92269
+ functionCap: "calendar-source",
92270
+ role: "services"
92271
+ }]
92559
92272
  },
92560
92273
  labels: { accountId: {
92561
92274
  label: "Google account",
@@ -92579,6 +92292,179 @@ function buildGoogleCalendarInstances(records, deps) {
92579
92292
  });
92580
92293
  }
92581
92294
  //#endregion
92295
+ //#region src/google-settings-copy.ts
92296
+ /**
92297
+ * Operator-facing setup copy for Connectivity → Calendars / Email.
92298
+ * Keep Connect as a same-origin relative link so the admin session cookie
92299
+ * rides along (an absolute Public URL from a LAN admin tab drops the cookie).
92300
+ * Root-relative hrefs must survive `sanitizeInfoHtml` in ui-library.
92301
+ */
92302
+ /** Enable APIs library in Google Cloud Console. */
92303
+ var GOOGLE_CONSOLE_APIS_URL = "https://console.cloud.google.com/apis/library";
92304
+ /** Create / manage OAuth Web clients. */
92305
+ var GOOGLE_CONSOLE_CREDENTIALS_URL = "https://console.cloud.google.com/apis/credentials";
92306
+ function redirectUriDisplay(publicOrigin) {
92307
+ if (publicOrigin === null || publicOrigin.length === 0) return `<public-hub>${OAUTH_CALLBACK_SUFFIX}`;
92308
+ return `${publicOrigin.replace(/\/$/, "")}${OAUTH_CALLBACK_SUFFIX}`;
92309
+ }
92310
+ function consoleLinksHtml$1() {
92311
+ return [
92312
+ `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
92313
+ ` · `,
92314
+ `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
92315
+ ].join("");
92316
+ }
92317
+ function introHtml(cap, publicOrigin) {
92318
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
92319
+ const console = consoleLinksHtml$1();
92320
+ if (cap === "smtp-provider") return [
92321
+ "<ol>",
92322
+ `<li>Open Google Cloud Console (${console}) and enable the <strong>Gmail API</strong> (and Calendar API if you also use Calendars).</li>`,
92323
+ "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
92324
+ `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
92325
+ "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
92326
+ `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
92327
+ "<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>",
92328
+ "</ol>"
92329
+ ].join("");
92330
+ if (cap === "calendar-source") return [
92331
+ "<ol>",
92332
+ `<li>Open Google Cloud Console (${console}) and enable the <strong>Google Calendar API</strong> (and Gmail API if you also use Email).</li>`,
92333
+ "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
92334
+ `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
92335
+ "<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
92336
+ `<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
92337
+ "<li>Optionally restrict which calendars are exposed with the filter below.</li>",
92338
+ "</ol>"
92339
+ ].join("");
92340
+ return [
92341
+ "<ol>",
92342
+ `<li>Open Google Cloud Console (${console}) and enable <strong>Google Calendar API</strong> and <strong>Gmail API</strong>.</li>`,
92343
+ "<li>Create a <strong>Web application</strong> OAuth client under Credentials.</li>",
92344
+ `<li>Register redirect URI: <code>${redirect}</code> (Network → Public endpoint).</li>`,
92345
+ `<li>Paste Client ID / secret, Save, then <a href="${OAUTH_START_PATH}">Connect Google</a>.</li>`,
92346
+ "<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>",
92347
+ "</ol>"
92348
+ ].join("");
92349
+ }
92350
+ function connectHtml(cap, publicOrigin) {
92351
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
92352
+ if (cap === "smtp-provider") return [
92353
+ `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
92354
+ "Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
92355
+ `Google must list exactly: <code>${redirect}</code>. `,
92356
+ "If an older Connect predates Gmail send, revoke CamStack in Google Account → Security and Connect again. ",
92357
+ `Console: ${consoleLinksHtml$1()}.`
92358
+ ].join("");
92359
+ if (cap === "calendar-source") return [
92360
+ `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
92361
+ "Connect requests <code>calendar.readonly</code> (and Gmail send for the Email page). ",
92362
+ `Google must list exactly: <code>${redirect}</code>. `,
92363
+ `Console: ${consoleLinksHtml$1()}.`
92364
+ ].join("");
92365
+ return [
92366
+ `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a>. `,
92367
+ `Redirect URI registered in Google Cloud must be <code>${redirect}</code>. `,
92368
+ `Console: ${consoleLinksHtml$1()}.`
92369
+ ].join("");
92370
+ }
92371
+ function escapeHtml$1(s) {
92372
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92373
+ }
92374
+ //#endregion
92375
+ //#region src/google-broker-settings-schema.ts
92376
+ /**
92377
+ * Per-kind broker creation/edit form for kind=`google`.
92378
+ * Account linking (Sign in) is driven by the kind's `connectPath`
92379
+ * on the instances page — not a hard-coded Connect link in this form.
92380
+ */
92381
+ function consoleLinksHtml() {
92382
+ return [
92383
+ `<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
92384
+ ` · `,
92385
+ `<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
92386
+ ].join("");
92387
+ }
92388
+ /** Setup steps shown on Add / Edit — includes Audience / Test users. */
92389
+ function googleBrokerSetupHtml(publicOrigin) {
92390
+ const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
92391
+ return [
92392
+ "<ol>",
92393
+ `<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>`,
92394
+ "<li>In that project, enable <strong>Google Calendar API</strong> and/or <strong>Gmail API</strong> (API library).</li>",
92395
+ "<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>",
92396
+ "<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
92397
+ `<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
92398
+ "<li>Paste Client ID and Client secret below. <strong>Test connection</strong> checks those with Google (not the account yet).</li>",
92399
+ "<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>",
92400
+ "</ol>"
92401
+ ].join("");
92402
+ }
92403
+ function buildGoogleBrokerSettingsSchema(publicOrigin) {
92404
+ return { sections: [
92405
+ {
92406
+ id: "setup-intro",
92407
+ title: "Setup",
92408
+ titleKey: "google-services:googleBrokerSettings.s_setup-intro.title",
92409
+ description: "Google Cloud Console — register the hub redirect before Add / Sign in.",
92410
+ descriptionKey: "google-services:googleBrokerSettings.s_setup-intro.description",
92411
+ columns: 1,
92412
+ fields: [{
92413
+ type: "info",
92414
+ key: "_setupSteps",
92415
+ label: "Steps",
92416
+ labelKey: "google-services:googleBrokerSettings.s_setup-intro._setupSteps.label",
92417
+ format: "html",
92418
+ content: googleBrokerSetupHtml(publicOrigin)
92419
+ }]
92420
+ },
92421
+ {
92422
+ id: "google-oauth-client",
92423
+ title: "Google OAuth client",
92424
+ titleKey: "google-services:googleBrokerSettings.s_google-oauth-client.title",
92425
+ description: "Web application client from Google Cloud Console. The same Cloud project can be reused across Google brokers.",
92426
+ descriptionKey: "google-services:googleBrokerSettings.s_google-oauth-client.description",
92427
+ columns: 1,
92428
+ fields: [{
92429
+ type: "text",
92430
+ key: "clientId",
92431
+ label: "Client ID",
92432
+ labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientId.label"
92433
+ }, {
92434
+ type: "password",
92435
+ key: "clientSecret",
92436
+ label: "Client secret",
92437
+ labelKey: "google-services:googleBrokerSettings.s_google-oauth-client.clientSecret.label",
92438
+ showToggle: true
92439
+ }]
92440
+ },
92441
+ {
92442
+ id: "gmail-send",
92443
+ title: "Gmail send",
92444
+ titleKey: "google-services:googleBrokerSettings.s_gmail-send.title",
92445
+ description: "From must be this connected account or a verified send-as. Disable Email on Connectivity to turn Gmail off without removing the broker.",
92446
+ descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.description",
92447
+ columns: 1,
92448
+ fields: [{
92449
+ type: "text",
92450
+ key: "fromAddress",
92451
+ label: "From address",
92452
+ labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.label",
92453
+ description: "Defaults to the connected Google account email when empty.",
92454
+ descriptionKey: "google-services:googleBrokerSettings.s_gmail-send.fromAddress.description"
92455
+ }, {
92456
+ type: "text",
92457
+ key: "fromName",
92458
+ label: "From display name (optional)",
92459
+ labelKey: "google-services:googleBrokerSettings.s_gmail-send.fromName.label"
92460
+ }]
92461
+ }
92462
+ ] };
92463
+ }
92464
+ function escapeHtml(s) {
92465
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92466
+ }
92467
+ //#endregion
92582
92468
  //#region src/google-instances.ts
92583
92469
  /**
92584
92470
  * The Google accounts as `integration-instances` (planning#408 P3): one instance per broker
@@ -92615,7 +92501,8 @@ function buildGoogleInstances(records, deps) {
92615
92501
  instanceMode: "multiple",
92616
92502
  kind: "account",
92617
92503
  canTest: true,
92618
- allowRestoreId: true
92504
+ allowRestoreId: true,
92505
+ kinds: [googleAccountKind(deps)]
92619
92506
  },
92620
92507
  formSchema: () => buildGoogleBrokerSettingsSchema(deps.publicOrigin()),
92621
92508
  initialBody: (name) => ({
@@ -93865,21 +93752,6 @@ var GoogleServicesAddon = class extends BaseAddon {
93865
93752
  this.publicOrigin = null;
93866
93753
  }
93867
93754
  }
93868
- /**
93869
- * Connectivity Calendars / Email: point at Brokers (no duplicate OAuth fields).
93870
- * Addon page without cap: same pointer + note.
93871
- */
93872
- globalSettingsSchema(cap) {
93873
- const scoped = asSettingsCap(cap);
93874
- return this.schema(buildGoogleFeatureSettingsSchema(scoped, this.publicOrigin));
93875
- }
93876
- async getGlobalSettings(overlay, cap, nodeId) {
93877
- await this.refreshPublicOrigin();
93878
- return super.getGlobalSettings(overlay, cap, nodeId);
93879
- }
93880
93755
  };
93881
- function asSettingsCap(cap) {
93882
- if (cap === "calendar-source" || cap === "smtp-provider") return cap;
93883
- }
93884
93756
  //#endregion
93885
- export { GOOGLE_OAUTH_SCOPES as C, GoogleServicesAddon, GoogleServicesAddon as default, GOOGLE_GMAIL_SEND_SCOPE as S, OAUTH_START_PATH as T, GoogleBrokerRegistry as _, refreshAccessToken as a, DEFAULT_GOOGLE_CONFIG as b, selectGmailAccount as c, mapGoogleEvent as d, GOOGLE_CONSOLE_APIS_URL as f, redirectUriDisplay as g, introHtml as h, needsRefresh as i, createGoogleCalendarApi as l, connectHtml as m, buildAuthorizeUrl as n, buildRawMime as o, GOOGLE_CONSOLE_CREDENTIALS_URL as p, exchangeCode as r, createGmailSmtpProvider as s, applyTokenResponse as t, createGoogleCalendarSource as u, parseStampedCalendarId as v, GoogleServicesConfigSchema as w, GOOGLE_BROKER_KIND as x, stampCalendarId as y };
93757
+ export { GOOGLE_OAUTH_SCOPES as C, GoogleServicesAddon, GoogleServicesAddon as default, GOOGLE_GMAIL_SEND_SCOPE as S, OAUTH_START_PATH as T, GoogleBrokerRegistry as _, redirectUriDisplay as a, DEFAULT_GOOGLE_CONFIG as b, exchangeCode as c, buildRawMime as d, createGmailSmtpProvider as f, mapGoogleEvent as g, createGoogleCalendarSource as h, introHtml as i, needsRefresh as l, createGoogleCalendarApi as m, GOOGLE_CONSOLE_CREDENTIALS_URL as n, applyTokenResponse as o, selectGmailAccount as p, connectHtml as r, buildAuthorizeUrl as s, GOOGLE_CONSOLE_APIS_URL as t, refreshAccessToken as u, parseStampedCalendarId as v, GoogleServicesConfigSchema as w, GOOGLE_BROKER_KIND as x, stampCalendarId as y };