@camstack/addon-google-services 0.2.38 → 0.2.39

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.
@@ -5985,7 +5985,7 @@ var ZodIssueCode = {
5985
5985
  var ZodFirstPartyTypeKind;
5986
5986
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5987
5987
  //#endregion
5988
- //#region ../types/dist/sleep-BaWhXZFm.mjs
5988
+ //#region ../types/dist/sleep-BX_MqzUe.mjs
5989
5989
  /**
5990
5990
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5991
5991
  * window to float samples (D455).
@@ -6585,7 +6585,7 @@ var NOT_RUN = {
6585
6585
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6586
6586
  * because the hub (and this addon's boot) must not hang on a migration.
6587
6587
  */
6588
- async function runAddonMigrations(host, migrations, logger) {
6588
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6589
6589
  const first = migrations[0];
6590
6590
  if (first === void 0) return NOT_RUN;
6591
6591
  const owner = first.meta.owner;
@@ -6614,7 +6614,9 @@ async function runAddonMigrations(host, migrations, logger) {
6614
6614
  else if (status === "blocked") blocked.push(id);
6615
6615
  else skipped.push(id);
6616
6616
  };
6617
+ const selected = options.only === void 0 ? null : new Set(options.only);
6617
6618
  for (const step of declaration.steps) {
6619
+ if (selected !== null && !selected.has(step.id)) continue;
6618
6620
  const bound = byId.get(step.id);
6619
6621
  if (bound === void 0 || step.action === "skip") {
6620
6622
  skipped.push(step.id);
@@ -6792,6 +6794,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6792
6794
  }
6793
6795
  return out;
6794
6796
  }
6797
+ /** Every scope, in the order the guard and the docs list them. */
6798
+ var SCOPED_SETTINGS_SCOPES = [
6799
+ "cluster",
6800
+ "device-defaults",
6801
+ "node"
6802
+ ];
6795
6803
  /**
6796
6804
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6797
6805
  *
@@ -7606,6 +7614,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
7606
7614
  /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
7607
7615
  var SECRET_CHANGE_DEBOUNCE_MS = 250;
7608
7616
  /**
7617
+ * Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
7618
+ * place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
7619
+ * through `api` (guard `check-migration-ports-surface`).
7620
+ */
7621
+ function migrationSet(catalogue, bindPorts) {
7622
+ return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
7623
+ }
7624
+ /** A mixed array is a mistake: the other kind would be dropped without a word. */
7625
+ function refuseMixed(form, kept, dropped) {
7626
+ if (dropped > 0) throw new Error(`runMigrations(${form}): ${dropped} of ${kept + dropped} entries are of the other kind and would be dropped; pass one kind per call`);
7627
+ }
7628
+ function catalogueOf(list) {
7629
+ const kept = list.filter((entry) => !("execute" in entry));
7630
+ refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
7631
+ return kept;
7632
+ }
7633
+ function boundOf(list) {
7634
+ const kept = list.filter((entry) => "execute" in entry);
7635
+ refuseMixed("bound migrations", kept.length, list.length - kept.length);
7636
+ return kept;
7637
+ }
7638
+ /**
7609
7639
  * Base class for CamStack addons. Eliminates settings boilerplate:
7610
7640
  *
7611
7641
  * - Typed `config` property with automatic resolution from store + defaults
@@ -7683,6 +7713,13 @@ var BaseAddon = class {
7683
7713
  */
7684
7714
  secretStatePaths = [];
7685
7715
  _addonSecretTree = null;
7716
+ /**
7717
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7718
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7719
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7720
+ */
7721
+ _scopedByCap = /* @__PURE__ */ new Map();
7722
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7686
7723
  _deviceSecretTree = null;
7687
7724
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7688
7725
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7752,6 +7789,7 @@ var BaseAddon = class {
7752
7789
  this._ctx = context;
7753
7790
  this._secretSettings = null;
7754
7791
  }
7792
+ await this.loadScopedSchemas();
7755
7793
  await this.resolveConfig();
7756
7794
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7757
7795
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7761,27 +7799,30 @@ var BaseAddon = class {
7761
7799
  const normalized = normalizeAddonInitResult(result);
7762
7800
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7763
7801
  this._registeredCapNames = providers.map((p) => p.capability.name);
7802
+ await this.loadScopedSchemas();
7764
7803
  return normalized;
7765
7804
  }
7805
+ async runMigrations(first, second, third) {
7806
+ if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
7807
+ return this.runBound(boundOf(first), second);
7808
+ }
7766
7809
  /**
7767
- * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7768
- * engine. Call it at the point of the boot where the data must already have the new
7769
- * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7770
- * restored devices — with the catalogue bound to the addon's ports
7771
- * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7772
- *
7773
- * An engine that cannot be reached or refuses the catalogue is logged and runs
7774
- * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7775
- * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7776
- * serve data whose shape it cannot trust; `tolerate` migrations never do.
7810
+ * Several catalogues of ONE owner in one declaration (the engine replaces an owner's
7811
+ * catalogue on every declare, so two calls would each judge the other's ids "newer").
7812
+ * Build each entry with `migrationSet(catalogue, (api) => ports)`.
7777
7813
  */
7778
- async runMigrations(migrations) {
7814
+ async runMigrationSets(sets, options) {
7815
+ const api = this.ctx.migrationApi;
7816
+ if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
7817
+ return this.runBound(sets.flatMap((set) => set.bind(api)), options);
7818
+ }
7819
+ async runBound(migrations, options) {
7779
7820
  const api = this.ctx.api.migrations;
7780
7821
  const report = await runAddonMigrations({
7781
7822
  declareOwner: (input) => api.declareOwner.mutate(input),
7782
7823
  startMigration: (input) => api.startMigration.mutate(input),
7783
7824
  finishMigration: (input) => api.finishMigration.mutate(input)
7784
- }, migrations, this.ctx.logger);
7825
+ }, migrations, this.ctx.logger, options);
7785
7826
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7786
7827
  return report;
7787
7828
  }
@@ -7845,6 +7886,16 @@ var BaseAddon = class {
7845
7886
  schema(schema) {
7846
7887
  return schema;
7847
7888
  }
7889
+ /**
7890
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7891
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7892
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7893
+ * must already know which keys are per node. Names the providers register at init are added
7894
+ * once known.
7895
+ */
7896
+ scopedSettingsCapabilities() {
7897
+ return [];
7898
+ }
7848
7899
  /** Override to provide global-level settings UI schema. */
7849
7900
  globalSettingsSchema(_cap) {
7850
7901
  return null;
@@ -7888,7 +7939,8 @@ var BaseAddon = class {
7888
7939
  return [];
7889
7940
  }
7890
7941
  async getGlobalSettings(overlay, cap, nodeId) {
7891
- const schema = this.globalSettingsSchema(cap);
7942
+ await this.loadScopedSchemas(cap);
7943
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7892
7944
  if (!schema) return { sections: [] };
7893
7945
  const projected = await this.readRawProjectedStore(nodeId, cap);
7894
7946
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7923,10 +7975,9 @@ var BaseAddon = class {
7923
7975
  *
7924
7976
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7925
7977
  * needs its connection fields, the default integration the addon's own sections. It is the
7926
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7927
- * unchanged (`integrations.setSettings` for an account integration).
7978
+ * SHAPE of the form only — the values live on the instance record.
7928
7979
  */
7929
- async integrationSettingsSchema(_instance) {
7980
+ async integrationFormSchema(_instance) {
7930
7981
  return null;
7931
7982
  }
7932
7983
  /**
@@ -7972,7 +8023,7 @@ var BaseAddon = class {
7972
8023
  * per-instance schema when it declares one, else its global schema (what they always showed).
7973
8024
  */
7974
8025
  async getIntegrationFormSchema(instance, nodeId) {
7975
- const scoped = await this.integrationSettingsSchema(instance);
8026
+ const scoped = await this.integrationFormSchema(instance);
7976
8027
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7977
8028
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7978
8029
  }
@@ -8020,6 +8071,7 @@ var BaseAddon = class {
8020
8071
  * resolved blob back is a no-op for every unchanged secret.
8021
8072
  */
8022
8073
  async resolveGlobalStore(nodeId, cap) {
8074
+ await this.loadScopedSchemas(cap);
8023
8075
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8024
8076
  return this.projectForNode(raw, nodeId, cap);
8025
8077
  }
@@ -8030,6 +8082,7 @@ var BaseAddon = class {
8030
8082
  }
8031
8083
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8032
8084
  async readRawProjectedStore(nodeId, cap) {
8085
+ await this.loadScopedSchemas(cap);
8033
8086
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8034
8087
  return this.projectForNode(raw, nodeId, cap);
8035
8088
  }
@@ -8132,6 +8185,7 @@ var BaseAddon = class {
8132
8185
  await this.applyGlobalPatch(patch, nodeId);
8133
8186
  }
8134
8187
  async applyGlobalPatch(barePatch, nodeId) {
8188
+ await this.loadScopedSchemas();
8135
8189
  const keys = this.perNodeKeys();
8136
8190
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8137
8191
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8145,7 +8199,8 @@ var BaseAddon = class {
8145
8199
  await this.resolveConfig();
8146
8200
  await this.onConfigChanged();
8147
8201
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8148
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8202
+ const legacy = this.globalSettingsSchema();
8203
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8149
8204
  }
8150
8205
  /**
8151
8206
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8153,14 +8208,67 @@ var BaseAddon = class {
8153
8208
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8154
8209
  * settings API behaves exactly like the legacy node-agnostic one.
8155
8210
  */
8156
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8211
+ /**
8212
+ * Load the scoped schemas of every capability this addon declares (the override and the
8213
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8214
+ * derived from the previous set. Idempotent.
8215
+ */
8216
+ async loadScopedSchemas(extraCap) {
8217
+ const caps = new Set([
8218
+ ...this.scopedSettingsCapabilities(),
8219
+ ...this._registeredCapNames,
8220
+ ...extraCap === void 0 ? [] : [extraCap]
8221
+ ]);
8222
+ let added = false;
8223
+ for (const cap of caps) {
8224
+ if (this._scopedByCap.has(cap)) continue;
8225
+ const loaded = [];
8226
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8227
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8228
+ if (schema !== null) loaded.push({
8229
+ scope,
8230
+ schema
8231
+ });
8232
+ }
8233
+ this._scopedByCap.set(cap, loaded);
8234
+ added = true;
8235
+ }
8236
+ if (!added) return;
8237
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8238
+ this._addonSecretTree = null;
8239
+ }
8240
+ /**
8241
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8242
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8243
+ */
8244
+ scopedFormSchema(cap) {
8245
+ const byId = /* @__PURE__ */ new Map();
8246
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8247
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8248
+ }
8249
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8250
+ loadedScopedSchemas(cap) {
8251
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8252
+ return [...this._scopedByCap.values()].flat();
8253
+ }
8254
+ /**
8255
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8256
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8257
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8258
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8259
+ * Call {@link loadScopedSchemas} first (the async seams do).
8260
+ */
8157
8261
  perNodeKeys(cap) {
8158
8262
  const cacheKey = cap ?? "";
8159
- const cached = this._perNodeKeysCache.get(cacheKey);
8263
+ const cached = this._nodeKeysByCap.get(cacheKey);
8160
8264
  if (cached) return cached;
8161
- const schema = this.globalSettingsSchema(cap);
8162
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8163
- this._perNodeKeysCache.set(cacheKey, keys);
8265
+ const legacy = this.globalSettingsSchema(cap);
8266
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8267
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8268
+ if (scope !== "node") continue;
8269
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8270
+ }
8271
+ this._nodeKeysByCap.set(cacheKey, keys);
8164
8272
  return keys;
8165
8273
  }
8166
8274
  /**
@@ -8170,10 +8278,9 @@ var BaseAddon = class {
8170
8278
  * time to return its response before the addon is torn down and
8171
8279
  * re-initialised by `AddonRegistryService.restartAddon`.
8172
8280
  */
8173
- maybeAutoRestart(patchKeys, schema) {
8174
- if (!schema) return;
8281
+ maybeAutoRestart(patchKeys, schemas) {
8175
8282
  const restartKeys = /* @__PURE__ */ new Set();
8176
- for (const section of schema.sections) for (const field of section.fields) {
8283
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8177
8284
  if (field.type === "separator" || field.type === "info") continue;
8178
8285
  if (field.requiresRestart) restartKeys.add(field.key);
8179
8286
  }
@@ -8369,12 +8476,14 @@ var BaseAddon = class {
8369
8476
  * here, in the addon's own process, so the hub keeps no copy of it.
8370
8477
  */
8371
8478
  async getDeclaredSettingsKeys() {
8479
+ await this.loadScopedSchemas();
8372
8480
  return { keys: [...this.declaredKeySet()].toSorted() };
8373
8481
  }
8374
8482
  declaredKeySet() {
8375
8483
  const keys = new Set(Object.keys(this.defaults));
8376
8484
  const schema = this.globalSettingsSchema();
8377
8485
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8486
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8378
8487
  for (const key of this._declaredStateKeys) keys.add(key);
8379
8488
  for (const path of this.secretStatePaths) {
8380
8489
  const root = path.split(/[.[]/, 1)[0];
@@ -8470,7 +8579,11 @@ var BaseAddon = class {
8470
8579
  }
8471
8580
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8472
8581
  addonSecretTree() {
8473
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8582
+ if (this._addonSecretTree === null) {
8583
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8584
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8585
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8586
+ }
8474
8587
  return this._addonSecretTree;
8475
8588
  }
8476
8589
  deviceSecretTree() {
@@ -8720,6 +8833,23 @@ function dropPerNodeFields(fields) {
8720
8833
  }
8721
8834
  return kept;
8722
8835
  }
8836
+ /** The keys of the value-holding fields, recursing into layout containers. */
8837
+ function collectValueFieldKeys(fields) {
8838
+ const collected = [];
8839
+ for (const field of fields) {
8840
+ if (field.type === "group") {
8841
+ collected.push(...collectValueFieldKeys(field.fields));
8842
+ continue;
8843
+ }
8844
+ if (field.type === "sub-tabs") {
8845
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8846
+ continue;
8847
+ }
8848
+ if (field.type === "separator" || field.type === "info") continue;
8849
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8850
+ }
8851
+ return collected;
8852
+ }
8723
8853
  function collectFieldKeys(fields) {
8724
8854
  const collected = [];
8725
8855
  for (const field of fields) {
@@ -9246,6 +9376,7 @@ function defineEntryKind(name, spec) {
9246
9376
  class: "config",
9247
9377
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9248
9378
  retention: { none: "an entry goes only when the operator deletes it" },
9379
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9249
9380
  ...spec.database === void 0 ? {} : { database: spec.database },
9250
9381
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9251
9382
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11925,7 +12056,7 @@ function userError(code, params, englishFallback) {
11925
12056
  return new CamstackError(code, params, englishFallback);
11926
12057
  }
11927
12058
  //#endregion
11928
- //#region ../types/dist/composition-CRNwcFYY.mjs
12059
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11929
12060
  /**
11930
12061
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11931
12062
  *
@@ -13523,7 +13654,7 @@ var RecordIntegrationInstances = class {
13523
13654
  return record === null ? null : this.viewOf(record);
13524
13655
  };
13525
13656
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13526
- createInstance = async ({ id, name, kind }) => {
13657
+ createInstance = async ({ id, name, kind, values }) => {
13527
13658
  const minted = id ?? this.hooks.mintId?.({
13528
13659
  name,
13529
13660
  ...kind !== void 0 ? { kind } : {}
@@ -13533,10 +13664,20 @@ var RecordIntegrationInstances = class {
13533
13664
  id: minted,
13534
13665
  data: this.hooks.initialBody(name, kind)
13535
13666
  }, { ifRev: 0 });
13667
+ const patch = this.patchOf(values ?? {});
13668
+ if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
13536
13669
  await this.hooks.onChanged?.(minted, "created");
13537
13670
  return { id: minted };
13538
13671
  };
13539
13672
  updateInstance = async ({ id, name, values }) => {
13673
+ const patch = this.patchOf(values);
13674
+ if (name !== void 0) patch[this.nameField] = name;
13675
+ await this.handle.patch(id, patch);
13676
+ await this.hooks.onChanged?.(id, "updated");
13677
+ return { ok: true };
13678
+ };
13679
+ /** The record patch a form `values` object means: secrets kept, cleared or replaced. */
13680
+ patchOf(values) {
13540
13681
  const patch = {};
13541
13682
  for (const [key, value] of Object.entries(values)) {
13542
13683
  if (value === void 0) continue;
@@ -13547,11 +13688,8 @@ var RecordIntegrationInstances = class {
13547
13688
  else if (isSecretClear(value)) patch[key] = null;
13548
13689
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13549
13690
  }
13550
- if (name !== void 0) patch[this.nameField] = name;
13551
- await this.handle.patch(id, patch);
13552
- await this.hooks.onChanged?.(id, "updated");
13553
- return { ok: true };
13554
- };
13691
+ return patch;
13692
+ }
13555
13693
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13556
13694
  setInstanceEnabled = async ({ id, enabled }) => {
13557
13695
  if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
@@ -14633,7 +14771,13 @@ var MigrationLedgerEntrySchema = object({
14633
14771
  * joined by the engine from the catalogues it knows this boot. Absent when no
14634
14772
  * catalogue lists the id (a newer database).
14635
14773
  */
14636
- meta: MigrationMetaSchema.optional()
14774
+ meta: MigrationMetaSchema.optional(),
14775
+ /**
14776
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14777
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14778
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14779
+ */
14780
+ ownerNotLoaded: boolean().optional()
14637
14781
  });
14638
14782
  /** A catalogued migration that is not applied, with where it stands. */
14639
14783
  var MigrationPendingEntrySchema = object({
@@ -15754,6 +15898,39 @@ var UnstampedEventMediaCountSchema = object({
15754
15898
  /** Sum across both, or `null` when either lane could not be counted. */
15755
15899
  total: number().int().nonnegative().nullable()
15756
15900
  }).nullable();
15901
+ /**
15902
+ * Where the stored event media sits, per storage location: how many media rows
15903
+ * and how many bytes carry each location id, how many carry none (legacy,
15904
+ * resolved to the default `eventMedia` location), and how many name a location
15905
+ * that was not asked about (a deleted or never-listed one).
15906
+ *
15907
+ * One covering-index aggregate per asked location plus the NULL one, and one
15908
+ * unfiltered total that scans the covering index: O(rows), not a walk of the
15909
+ * rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
15910
+ * it is a deleted location only when every location id was asked.
15911
+ * `null` is "could not be measured", never zero.
15912
+ */
15913
+ var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
15914
+ var MediaLocationCensusSchema = object({
15915
+ locations: array(object({
15916
+ locationId: string(),
15917
+ rows: number().int().nonnegative(),
15918
+ bytes: number().nonnegative()
15919
+ })).readonly(),
15920
+ unstamped: object({
15921
+ rows: number().int().nonnegative(),
15922
+ bytes: number().nonnegative()
15923
+ }),
15924
+ /** Rows stamped with a location id that was not in the asked list. */
15925
+ unlisted: object({
15926
+ rows: number().int().nonnegative(),
15927
+ bytes: number().nonnegative()
15928
+ }),
15929
+ total: object({
15930
+ rows: number().int().nonnegative(),
15931
+ bytes: number().nonnegative()
15932
+ })
15933
+ });
15757
15934
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
15758
15935
  /** The independently selectable logical storage classes — every class
15759
15936
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -46293,52 +46470,7 @@ DeviceType.Light, method(object({
46293
46470
  level: number().min(0).max(100),
46294
46471
  lastChangedAt: number()
46295
46472
  });
46296
- /**
46297
- * `broker` — unified pub/sub broker registry, system-scoped collection.
46298
- *
46299
- * The cap models any kind-tagged message broker the user wants to
46300
- * register and that other addons might consume. The first two kinds
46301
- * are MQTT (mosquitto / aedes embedded / cloud bridge) and
46302
- * Home Assistant (HA WebSocket — subscribe_entities + call_service).
46303
- * Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
46304
- * the same surface.
46305
- *
46306
- * Why one cap, not one cap per kind:
46307
- * - The integrations page wants a single table of "every broker"
46308
- * across kinds; a unified cap drives it without join logic.
46309
- * - Generic admin operations (add / remove / test / status) are
46310
- * identical across kinds — duplicating them per cap was busywork.
46311
- * - The `network-access` cap precedent: same interface, many
46312
- * `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
46313
- *
46314
- * What stays kind-specific:
46315
- * - `getBrokerConfig` payload (kind decides whether it returns
46316
- * mqtt URL+creds, HA baseUrl+token, …) — typed as
46317
- * `Record<string, unknown>` at the wire; consumers narrow.
46318
- * - `publish` / `subscribe` / `getState` arguments (target / filter /
46319
- * key) — also typed as `Record<string, unknown>` so each kind can
46320
- * evolve its surface (MQTT topic+qos vs HA entity_id+domain)
46321
- * without rebreaking the cap signature.
46322
- * - `add` `settings` payload (different fields per kind, validated by
46323
- * the kind-specific provider).
46324
- *
46325
- * Bidirectionality:
46326
- * - `publish` is RPC — synchronous result (provider-defined).
46327
- * - `subscribe` returns a subscription handle; the actual message
46328
- * stream flows over the typed event-bus as `broker.message` events
46329
- * keyed by `(brokerId, subscriptionId)`. Consumers filter in their
46330
- * event handler. Subscriptions persist across reconnects — the
46331
- * provider re-subscribes upstream after a transport drop.
46332
- *
46333
- * Why event-bus push (not a subscription RPC stream):
46334
- * - Matches the rest of CamStack's D8 contract — events for telemetry,
46335
- * RPC for loss-is-a-bug. Broker messages are telemetry (a single
46336
- * drop is recoverable via `getState({key})`).
46337
- * - Frees `broker.subscribe` from holding a long-lived RPC channel.
46338
- * - Cross-process: events already route through `$event-bus`; no
46339
- * per-broker wire-shaping needed.
46340
- */
46341
- var BrokerStatusEnum = _enum([
46473
+ _enum([
46342
46474
  "connected",
46343
46475
  "disconnected",
46344
46476
  "connecting",
@@ -46346,141 +46478,12 @@ var BrokerStatusEnum = _enum([
46346
46478
  "unreachable",
46347
46479
  "error"
46348
46480
  ]);
46349
- var BrokerInfoSchema = object({
46350
- /** Stable broker id. Persisted; survives addon restarts. */
46351
- id: string(),
46352
- /** Addon id of the provider that OWNS this broker.
46353
- *
46354
- * The `broker` cap is a system-scoped collection: several addons
46355
- * register a `broker` provider, each owning a DISJOINT set of
46356
- * brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
46357
- * `ha_*`). A broker therefore belongs to exactly one addon — like a
46358
- * device belongs to one integration. The admin UI threads this id
46359
- * back as the `{ addonId }` system-collection selector on every
46360
- * id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
46361
- * `testConnection`), so the call routes to the OWNING provider
46362
- * instead of defaulting to the first-registered one. */
46363
- addonId: string(),
46364
- /** Human-readable name (operator-chosen at add-time). */
46365
- name: string(),
46366
- /** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
46367
- kind: string(),
46368
- status: BrokerStatusEnum,
46369
- /** Free-form provider-specific info (HA version, MQTT broker
46370
- * flavour, latency, mTLS active, …). */
46371
- info: record(string(), unknown()),
46372
- /** Ms epoch of the last connection probe / status update. */
46373
- lastCheckedAt: number().nullable(),
46374
- /** Last error message, when `status` indicates failure. */
46375
- error: string().nullable()
46376
- });
46377
46481
  var RegistryStatusSchema = object({
46378
46482
  brokerCount: number().int().nonnegative(),
46379
46483
  connectedCount: number().int().nonnegative()
46380
46484
  });
46381
- /**
46382
- * One entry per `broker` provider — which addon provides which broker
46383
- * kind(s). The unified create picker reads this to know, for each
46384
- * (addonId, kind) pair the operator can pick, where to route the
46385
- * follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
46386
- * `{ addonId }` selector). A provider only advertises a kind here when
46387
- * it implements `getSettingsSchema` for that kind (so the picker never
46388
- * offers a kind whose creation form can't be rendered).
46389
- */
46390
- var BrokerProviderInfoSchema = object({
46391
- /** Addon id of the `broker` provider this entry describes. */
46392
- addonId: string(),
46393
- /** Broker kinds this provider can create, with a display label. */
46394
- kinds: array(object({
46395
- kind: string(),
46396
- label: string(),
46397
- /**
46398
- * Relative hub path to start an interactive account link (OAuth popup).
46399
- * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
46400
- */
46401
- interactiveConnectPath: string().min(1).optional(),
46402
- /** Relative hub path to disconnect the linked account (same popup pattern). */
46403
- interactiveDisconnectPath: string().min(1).optional(),
46404
- /**
46405
- * The kind signs in through a server-held interactive session instead of
46406
- * (or before) typed credentials. `basePath` is a hub-relative route set
46407
- * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
46408
- * contract: `POST {basePath}/start` (body: the form's settings) answers
46409
- * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
46410
- * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
46411
- * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
46412
- * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
46413
- * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
46414
- * (`{ sessionId }`) drops the session. The dialog that edits a broker
46415
- * offers it as the default path; the fields the session fills are
46416
- * marked `filledBySignIn` in the kind's settings schema.
46417
- *
46418
- * `methods` lists the ways the session can sign in, recommended first:
46419
- * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
46420
- * settings) answering `{ sessionId, userCode, verificationUrl,
46421
- * expiresAt }` — the operator types `userCode` at `verificationUrl` on
46422
- * any device, and the same `status` / `commit` / `cancel` routes follow
46423
- * (status then carries `method`, `userCode`, `verificationUrl`,
46424
- * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
46425
- * `'proxy'` is the `{basePath}/start` flow above. Absent means
46426
- * `['proxy']`.
46427
- */
46428
- signIn: object({
46429
- basePath: string().min(1),
46430
- methods: array(_enum(["device-code", "proxy"])).optional()
46431
- }).optional()
46432
- }))
46433
- });
46434
- var ListInputSchema = object({
46435
- /** Optional kind filter — `list({kind:'home-assistant'})` returns
46436
- * only HA brokers. Omit to list every broker. */
46437
- kind: string().optional() });
46438
46485
  var GetInputSchema = object({ id: string() });
46439
- var AddInputSchema = object({
46440
- kind: string().min(1),
46441
- name: string().min(1),
46442
- /**
46443
- * ADOPT an existing id instead of minting a new one.
46444
- *
46445
- * Written the day this cost an outage. A broker's id is not a detail: HA
46446
- * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
46447
- * lost from config and re-added as `ha_001` leaves every one of its devices
46448
- * bound to a broker that no longer exists. Re-entering the password under the
46449
- * ORIGINAL id turns a multi-step device migration back into re-entering a
46450
- * password.
46451
- *
46452
- * A provider MUST refuse an id that is already in use — adopting a live
46453
- * broker's id would silently take it over.
46454
- */
46455
- id: string().min(1).optional(),
46456
- /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
46457
- * `{baseUrl,accessToken}`). Validated by the kind-specific provider
46458
- * branch on receipt — invalid shape rejects the add. */
46459
- settings: record(string(), unknown())
46460
- });
46461
- var AddResultSchema = object({ id: string() });
46462
- var RemoveInputSchema = object({ id: string() });
46463
- var TestConnectionResultSchema = discriminatedUnion("ok", [object({
46464
- ok: literal(true),
46465
- latencyMs: number().nonnegative()
46466
- }), object({
46467
- ok: literal(false),
46468
- error: string()
46469
- })]);
46470
46486
  var SettingsRecordSchema$1 = record(string(), unknown());
46471
- var SettingsSchemaInputSchema = object({ kind: string() });
46472
- var TestSettingsInputSchema = object({
46473
- kind: string(),
46474
- settings: SettingsRecordSchema$1
46475
- });
46476
- var TestSettingsResultSchema = discriminatedUnion("ok", [object({
46477
- ok: literal(true),
46478
- latencyMs: number().nonnegative().optional()
46479
- }).strict(), object({
46480
- ok: literal(false),
46481
- error: string()
46482
- })]);
46483
- var SettingsSchemaResultSchema = unknown().nullable();
46484
46487
  var PublishInputSchema = object({
46485
46488
  brokerId: string(),
46486
46489
  /** Kind-specific routing target.
@@ -46524,51 +46527,12 @@ var brokerCapability = {
46524
46527
  kind: "poll"
46525
46528
  },
46526
46529
  methods: {
46527
- list: method(ListInputSchema, array(BrokerInfoSchema)),
46528
- get: method(GetInputSchema, BrokerInfoSchema.nullable()),
46529
- /** Enumerate which addon provides which broker kind(s) for the
46530
- * unified create picker. The auto-mount fans this array across
46531
- * every registered `broker` provider (array-output method), so the
46532
- * picker sees every kind from every provider in one call. */
46533
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
46534
- add: method(AddInputSchema, AddResultSchema, {
46535
- kind: "mutation",
46536
- auth: "admin"
46537
- }),
46538
- remove: method(RemoveInputSchema, _void(), {
46539
- kind: "mutation",
46540
- auth: "admin"
46541
- }),
46542
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
46543
- kind: "mutation",
46544
- auth: "admin"
46545
- }),
46546
- /** Read the persisted settings record for a broker (kind-specific
46547
- * shape). Admin-only — settings may contain secrets. Returns `null`
46548
- * when the broker id is unknown to the provider (the collection
46549
- * fallback may route a foreign id to the first provider). */
46550
- getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46551
- /** Overwrite the persisted settings record. The kind-specific
46552
- * provider validates the shape and applies the change (reconnects
46553
- * if credentials changed). */
46554
- setSettings: method(object({
46555
- id: string(),
46556
- settings: SettingsRecordSchema$1
46557
- }), _void(), {
46558
- kind: "mutation",
46559
- auth: "admin"
46560
- }),
46561
46530
  /** Returns the kind-specific connection config the consumer needs
46562
46531
  * to open its own client (MQTT pattern: `{url, username, password,
46563
46532
  * clientIdPrefix}`). HA providers MAY return the auth envelope
46564
46533
  * but typical HA consumers use `publish` / `subscribe` instead.
46565
46534
  * Returns `null` when the broker id is unknown to the provider. */
46566
46535
  getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46567
- getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
46568
- testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
46569
- kind: "mutation",
46570
- auth: "admin"
46571
- }),
46572
46536
  publish: method(PublishInputSchema, unknown(), {
46573
46537
  kind: "mutation",
46574
46538
  auth: "admin"
@@ -49202,6 +49166,9 @@ var RunningProposalEvidenceSchema = object({
49202
49166
  runsFound: number().int().nonnegative(),
49203
49167
  pausesFound: number().int().nonnegative(),
49204
49168
  longestPauseMs: number().nonnegative(),
49169
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
49170
+ pulsesFound: number().int().nonnegative(),
49171
+ longestPulseMs: number().nonnegative(),
49205
49172
  idleFloorW: number(),
49206
49173
  noiseCeilingW: number(),
49207
49174
  plateauW: number(),
@@ -50648,7 +50615,14 @@ var DiscoveryCandidateSchema = object({
50648
50615
  /** `stableId` of the gateway candidate this device was found BEHIND. */
50649
50616
  parentStableId: string().optional(),
50650
50617
  /** `false` = shown for context but not pickable. Default true. */
50651
- selectable: boolean().optional()
50618
+ selectable: boolean().optional(),
50619
+ /**
50620
+ * Keys of the provider's creation form (`getChildCreationSchema`) this candidate
50621
+ * cannot be adopted without — a key for an encrypted board, a pairing code. The
50622
+ * admin asks for exactly these, on the creation form prefilled with
50623
+ * `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
50624
+ */
50625
+ needsParams: array(string()).optional()
50652
50626
  });
50653
50627
  /**
50654
50628
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -50707,12 +50681,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
50707
50681
  }), array(DiscoveryCandidateSchema), {
50708
50682
  kind: "mutation",
50709
50683
  auth: "admin"
50710
- }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
50684
+ }), method(object({}), CreationSchemaOutputSchema), method(object({
50685
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50686
+ integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
50687
+ candidate: DiscoveryCandidateSchema,
50688
+ /** The integration the operator adopts into; a provider with several instances needs it. */
50689
+ integrationId: string().optional()
50690
+ }), DeviceSummarySchema, {
50711
50691
  kind: "mutation",
50712
50692
  auth: "admin"
50713
- }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
50693
+ }), method(object({}), boolean()), method(object({
50714
50694
  type: _enum(DeviceType),
50715
- config: record(string(), unknown())
50695
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50696
+ integrationId: string().optional()
50697
+ }), CreationSchemaOutputSchema), method(object({
50698
+ type: _enum(DeviceType),
50699
+ config: record(string(), unknown()),
50700
+ /** The integration the operator adds the device to; a provider with several instances needs it. */
50701
+ integrationId: string().optional()
50716
50702
  }), DeviceSummarySchema, {
50717
50703
  kind: "mutation",
50718
50704
  auth: "admin"
@@ -51422,7 +51408,10 @@ method(object({
51422
51408
  auth: "admin"
51423
51409
  }), method(object({
51424
51410
  addonId: string(),
51425
- type: _enum(DeviceType)
51411
+ type: _enum(DeviceType),
51412
+ /** The integration the form is for; a provider whose integrations differ (a hub
51413
+ * integration takes no manual add) answers per integration. Absent = the addon's own. */
51414
+ integrationId: string().optional()
51426
51415
  }), unknown().nullable()), method(object({
51427
51416
  addonId: string(),
51428
51417
  type: _enum(DeviceType),
@@ -51506,7 +51495,11 @@ method(object({
51506
51495
  kind: "mutation",
51507
51496
  auth: "admin",
51508
51497
  timeoutMs: 12e4
51509
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51498
+ }), method(object({
51499
+ addonId: string(),
51500
+ /** The integration the Add-Device dialog is open on; absent = the addon's own. */
51501
+ integrationId: string().optional()
51502
+ }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51510
51503
  deviceId: number(),
51511
51504
  key: string(),
51512
51505
  value: unknown()
@@ -54102,7 +54095,7 @@ var TestConnectionStatusEnum = _enum([
54102
54095
  "inconclusive",
54103
54096
  "unsupported"
54104
54097
  ]);
54105
- var TestConnectionResultSchema$1 = object({
54098
+ var TestConnectionResultSchema = object({
54106
54099
  /** True ONLY for `validated`. Never true for a test that did not run. */
54107
54100
  success: boolean(),
54108
54101
  error: string().optional(),
@@ -54123,11 +54116,7 @@ var CreateIntegrationInputSchema = object({
54123
54116
  info: record(string(), unknown()).optional(),
54124
54117
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54125
54118
  originStableId: string().optional(),
54126
- settings: record(string(), unknown()).optional(),
54127
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
54128
- * the keychain, not the table (D787). */
54129
- secretKeys: array(string()).optional(),
54130
- skipRestart: boolean().optional()
54119
+ settings: record(string(), unknown()).optional()
54131
54120
  });
54132
54121
  var UpdateIntegrationInputSchema = object({
54133
54122
  id: string(),
@@ -54148,20 +54137,10 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54148
54137
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54149
54138
  kind: "mutation",
54150
54139
  auth: "admin"
54151
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
54152
- id: string(),
54153
- settings: record(string(), unknown()),
54154
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
54155
- secretKeys: array(string()).optional(),
54156
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
54157
- skipRestart: boolean().optional()
54158
- }), object({ success: boolean() }), {
54159
- kind: "mutation",
54160
- auth: "admin"
54161
54140
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54162
54141
  addonId: string(),
54163
54142
  settings: record(string(), unknown())
54164
- }), TestConnectionResultSchema$1, {
54143
+ }), TestConnectionResultSchema, {
54165
54144
  kind: "mutation",
54166
54145
  auth: "admin"
54167
54146
  });
@@ -54188,6 +54167,14 @@ var ConfigSchemaPassthrough$3 = custom();
54188
54167
  var IntegrationInstanceIdSchema = string().min(1);
54189
54168
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54190
54169
  /**
54170
+ * What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
54171
+ * instance is an integration that brings devices (the registry owns its on/off switch);
54172
+ * `services`: a connection configured from the addon's own function page, never listed on
54173
+ * Integrations.
54174
+ * It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
54175
+ */
54176
+ var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
54177
+ /**
54191
54178
  * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54192
54179
  * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54193
54180
  * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
@@ -54198,6 +54185,22 @@ var IntegrationInstanceKindSchema = object({
54198
54185
  /** Stable id of the kind, sent back as `createInstance.kind`. */
54199
54186
  id: string().min(1),
54200
54187
  label: string().min(1),
54188
+ /**
54189
+ * The function capability this kind is configured for (`broker`, `smtp-provider`,
54190
+ * `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
54191
+ * provider with more than one section must name it on every kind.
54192
+ */
54193
+ functionCap: string().min(1),
54194
+ /**
54195
+ * For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
54196
+ * (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
54197
+ * finds the instances whose kind names it; absent takes the kind `id`.
54198
+ */
54199
+ brokerKind: string().min(1).optional(),
54200
+ /** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
54201
+ role: IntegrationInstanceRoleSchema.optional(),
54202
+ /** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
54203
+ canTest: boolean().optional(),
54201
54204
  /** The kind signs in through a server-held interactive session. */
54202
54205
  signIn: object({
54203
54206
  basePath: string().min(1),
@@ -54210,11 +54213,7 @@ var IntegrationInstanceKindSchema = object({
54210
54213
  });
54211
54214
  var IntegrationInstanceDescriptionSchema = object({
54212
54215
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54213
- role: _enum([
54214
- "devices",
54215
- "services",
54216
- "both"
54217
- ]),
54216
+ role: IntegrationInstanceRoleSchema,
54218
54217
  /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54219
54218
  discovery: object({
54220
54219
  mode: _enum([
@@ -54289,7 +54288,11 @@ var integrationInstancesCapability = {
54289
54288
  methods: {
54290
54289
  describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
54291
54290
  /** The form; `id: null` is the create form. */
54292
- instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54291
+ instanceFormSchema: method(object({
54292
+ id: IntegrationInstanceIdSchema.nullable(),
54293
+ /** The kind of the create form (`id: null`) of a provider with several. */
54294
+ kind: string().min(1).optional()
54295
+ }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54293
54296
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54294
54297
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54295
54298
  /**
@@ -54299,7 +54302,9 @@ var integrationInstancesCapability = {
54299
54302
  createInstance: method(object({
54300
54303
  id: IntegrationInstanceIdSchema.optional(),
54301
54304
  name: string().min(1),
54302
- kind: string().min(1).optional()
54305
+ kind: string().min(1).optional(),
54306
+ /** Form values written with the record (a link picked in the create dialog). */
54307
+ values: record(string(), unknown()).optional()
54303
54308
  }), object({ id: string() }), {
54304
54309
  kind: "mutation",
54305
54310
  auth: "admin"
@@ -54326,8 +54331,10 @@ var integrationInstancesCapability = {
54326
54331
  }),
54327
54332
  testInstance: optionalMethod(object({
54328
54333
  id: IntegrationInstanceIdSchema.nullable(),
54329
- values: record(string(), unknown())
54330
- }), TestConnectionResultSchema$1, {
54334
+ values: record(string(), unknown()),
54335
+ /** The kind to test when `id` is null and the provider has several. */
54336
+ kind: string().min(1).optional()
54337
+ }), TestConnectionResultSchema, {
54331
54338
  kind: "mutation",
54332
54339
  auth: "admin"
54333
54340
  })
@@ -56832,6 +56839,380 @@ var SystemMetricsSchema = object({
56832
56839
  gpuMemoryPercent: number().optional()
56833
56840
  });
56834
56841
  method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
56842
+ /**
56843
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
56844
+ * config documents, mirrors. The third storage API next to `ctx.settings`
56845
+ * (operator intent, D870) and device-history (numbers over time, D850).
56846
+ *
56847
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
56848
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
56849
+ * that with the addon id closed over). It is deliberately NOT in any input
56850
+ * schema, so a session can never supply it, and a call with no stamp is refused
56851
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
56852
+ * addons go through the owner's capability.
56853
+ *
56854
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
56855
+ * never `[]`. Secret fields are never on the wire: a read shows the
56856
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
56857
+ * resolves itself, and a write may carry only a keychain marker bound to the
56858
+ * row's own ref.
56859
+ *
56860
+ * Internal: addons reach it through `ctx.records`, never directly.
56861
+ */
56862
+ var RecordIdSchema = string().min(1).max(256);
56863
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
56864
+ var RecordBodySchema = record(string(), unknown());
56865
+ var RecordInputSchema = object({
56866
+ id: RecordIdSchema,
56867
+ /** The device this row belongs to (D865 removal, per-device reads). */
56868
+ deviceId: number().int().optional(),
56869
+ /** The domain clock, ms: observed / created / opened. */
56870
+ at: number().int().optional(),
56871
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
56872
+ expiresAt: number().int().optional(),
56873
+ data: RecordBodySchema
56874
+ });
56875
+ var RecordViewSchema = object({
56876
+ id: string(),
56877
+ deviceId: number().int().nullable(),
56878
+ at: number().nullable(),
56879
+ expiresAt: number().nullable(),
56880
+ rev: number().int(),
56881
+ updatedAt: number(),
56882
+ data: RecordBodySchema
56883
+ });
56884
+ /** Declared index fields only. Anything else does not compile, by design. */
56885
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
56886
+ string(),
56887
+ number(),
56888
+ boolean(),
56889
+ object({ in: array(union([
56890
+ string(),
56891
+ number(),
56892
+ boolean()
56893
+ ])).min(1).max(RECORD_BATCH_MAX) }),
56894
+ object({
56895
+ gt: union([string(), number()]).optional(),
56896
+ gte: union([string(), number()]).optional(),
56897
+ lt: union([string(), number()]).optional(),
56898
+ lte: union([string(), number()]).optional()
56899
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
56900
+ ]));
56901
+ var KindShape = { kind: RecordKindNameSchema };
56902
+ var TimeWindowShape = {
56903
+ deviceId: number().int().optional(),
56904
+ atFrom: number().optional(),
56905
+ atTo: number().optional()
56906
+ };
56907
+ var RecordQueryInputSchema = object({
56908
+ ...KindShape,
56909
+ ...TimeWindowShape,
56910
+ where: RecordWhereSchema.optional(),
56911
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
56912
+ order: _enum(["at", "id"]).optional(),
56913
+ desc: boolean().optional(),
56914
+ limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
56915
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
56916
+ cursor: string().min(1).optional()
56917
+ });
56918
+ var RecordQueryResultSchema = object({
56919
+ records: array(RecordViewSchema),
56920
+ nextCursor: string().nullable()
56921
+ });
56922
+ var RecordKindStateSchema = _enum([
56923
+ "created",
56924
+ "unchanged",
56925
+ "updated"
56926
+ ]);
56927
+ var DeclareKindResultSchema = object({
56928
+ kindId: number().int(),
56929
+ state: RecordKindStateSchema,
56930
+ createdIndexes: array(string())
56931
+ });
56932
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
56933
+ var SecretRefsList = array(string());
56934
+ /**
56935
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
56936
+ * All ops of one call share ONE database file and ONE `transactFamily`.
56937
+ */
56938
+ var RecordTransactOpSchema = discriminatedUnion("op", [
56939
+ object({
56940
+ op: literal("put"),
56941
+ kind: RecordKindNameSchema,
56942
+ record: RecordInputSchema,
56943
+ ifRev: number().int().min(0).optional()
56944
+ }),
56945
+ object({
56946
+ op: literal("patch"),
56947
+ kind: RecordKindNameSchema,
56948
+ id: RecordIdSchema,
56949
+ data: RecordBodySchema,
56950
+ at: number().int().nullable().optional(),
56951
+ expiresAt: number().int().nullable().optional(),
56952
+ ifRev: number().int().min(1).optional()
56953
+ }),
56954
+ object({
56955
+ op: literal("delete"),
56956
+ kind: RecordKindNameSchema,
56957
+ id: RecordIdSchema
56958
+ })
56959
+ ]);
56960
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
56961
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
56962
+ kind: "mutation",
56963
+ auth: "admin"
56964
+ }), method(object({
56965
+ ...KindShape,
56966
+ id: RecordIdSchema
56967
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
56968
+ ...KindShape,
56969
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56970
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
56971
+ ...KindShape,
56972
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56973
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
56974
+ ...KindShape,
56975
+ record: RecordInputSchema,
56976
+ /** Compare-and-set. `0` means "the row must not exist". */
56977
+ ifRev: number().int().min(0).optional()
56978
+ }), object({
56979
+ rev: number().int(),
56980
+ releasedRefs: SecretRefsList
56981
+ }), {
56982
+ kind: "mutation",
56983
+ auth: "admin"
56984
+ }), method(object({
56985
+ ...KindShape,
56986
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
56987
+ }), object({
56988
+ count: number().int(),
56989
+ releasedRefs: SecretRefsList
56990
+ }), {
56991
+ kind: "mutation",
56992
+ auth: "admin"
56993
+ }), method(object({
56994
+ ...KindShape,
56995
+ id: RecordIdSchema,
56996
+ data: RecordBodySchema,
56997
+ at: number().int().nullable().optional(),
56998
+ expiresAt: number().int().nullable().optional(),
56999
+ ifRev: number().int().min(1).optional()
57000
+ }), object({
57001
+ rev: number().int(),
57002
+ releasedRefs: SecretRefsList
57003
+ }), {
57004
+ kind: "mutation",
57005
+ auth: "admin"
57006
+ }), method(object({
57007
+ ...KindShape,
57008
+ id: RecordIdSchema
57009
+ }), object({
57010
+ deleted: boolean(),
57011
+ secretRefs: SecretRefsList
57012
+ }), {
57013
+ kind: "mutation",
57014
+ auth: "admin"
57015
+ }), method(object({
57016
+ ...KindShape,
57017
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
57018
+ }), object({
57019
+ deleted: number().int(),
57020
+ secretRefs: SecretRefsList
57021
+ }), {
57022
+ kind: "mutation",
57023
+ auth: "admin"
57024
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
57025
+ ...KindShape,
57026
+ ...TimeWindowShape,
57027
+ where: RecordWhereSchema.optional()
57028
+ }), object({ count: number().int() }), { auth: "admin" }), method(object({
57029
+ ...KindShape,
57030
+ deviceId: number().int().optional(),
57031
+ from: number(),
57032
+ to: number(),
57033
+ bucketMs: number().int().positive()
57034
+ }), object({ buckets: array(object({
57035
+ bucketStart: number(),
57036
+ count: number().int()
57037
+ })) }), { auth: "admin" }), method(object({
57038
+ ...KindShape,
57039
+ op: _enum([
57040
+ "sum",
57041
+ "min",
57042
+ "max",
57043
+ "count"
57044
+ ]),
57045
+ field: RecordFieldPathSchema.optional(),
57046
+ groupBy: RecordFieldPathSchema.optional(),
57047
+ where: RecordWhereSchema.optional()
57048
+ }), object({ rows: array(object({
57049
+ group: union([
57050
+ string(),
57051
+ number(),
57052
+ _null()
57053
+ ]),
57054
+ value: number().nullable()
57055
+ })) }), { auth: "admin" }), method(object({
57056
+ ...KindShape,
57057
+ deviceId: number().int().optional(),
57058
+ atBefore: number().optional(),
57059
+ where: RecordWhereSchema.optional()
57060
+ }), object({
57061
+ deleted: number().int(),
57062
+ secretRefs: SecretRefsList
57063
+ }), {
57064
+ kind: "mutation",
57065
+ auth: "admin"
57066
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
57067
+ results: array(object({
57068
+ rev: number().int().nullable(),
57069
+ deleted: boolean()
57070
+ })),
57071
+ releasedRefs: SecretRefsList,
57072
+ secretRefs: SecretRefsList
57073
+ }), {
57074
+ kind: "mutation",
57075
+ auth: "admin"
57076
+ }), method(object({
57077
+ deviceId: number().int(),
57078
+ kinds: array(RecordKindNameSchema).optional()
57079
+ }), object({
57080
+ deleted: record(string(), number().int()),
57081
+ secretRefs: SecretRefsList
57082
+ }), {
57083
+ kind: "mutation",
57084
+ auth: "admin"
57085
+ }), method(object(KindShape), object({ id: number().int() }), {
57086
+ kind: "mutation",
57087
+ auth: "admin"
57088
+ });
57089
+ /**
57090
+ * The wire shapes of the migration API, version 1 (planning#447).
57091
+ *
57092
+ * A migration is code that outlives the server it was written against: an addon at
57093
+ * version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
57094
+ * the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
57095
+ * is the contract of every method of it, one Zod pair per method.
57096
+ *
57097
+ * APPEND-ONLY. A released method is never removed and never changes meaning: an input
57098
+ * field is never removed or made required, an enum never narrows, an output field is
57099
+ * never removed. A change that cannot be expressed that way is `migration-api.v2`, next
57100
+ * to this file. `scripts/check-migration-api-append-only.ts` holds the line against
57101
+ * `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
57102
+ * `it('<id>'` in `migration-api.contract.spec.ts`.
57103
+ */
57104
+ var Rows = record(string(), unknown());
57105
+ var IntegrationId = object({ integrationId: string().min(1) });
57106
+ var RecordId = object({ id: string().min(1) });
57107
+ var RecordRowSchema = object({
57108
+ id: string(),
57109
+ deviceId: number().int().nullable(),
57110
+ at: number().nullable(),
57111
+ expiresAt: number().nullable(),
57112
+ rev: number().int(),
57113
+ updatedAt: number(),
57114
+ data: Rows
57115
+ });
57116
+ var RecordPutShape = {
57117
+ id: string().min(1),
57118
+ deviceId: number().int().optional(),
57119
+ at: number().int().optional(),
57120
+ expiresAt: number().int().optional(),
57121
+ data: Rows
57122
+ };
57123
+ var MigrationIntegrationSchema = object({
57124
+ id: string(),
57125
+ addonId: string(),
57126
+ name: string(),
57127
+ info: Rows,
57128
+ originStableId: string().optional()
57129
+ });
57130
+ var MigrationDeviceSchema = object({
57131
+ id: number().int(),
57132
+ integrationId: string().nullable().optional(),
57133
+ /** The owning device; null or absent for a root. */
57134
+ parentDeviceId: number().int().nullable().optional(),
57135
+ type: string().optional(),
57136
+ name: string().optional(),
57137
+ stableId: string().optional()
57138
+ });
57139
+ /** What the engine says about the migration API it serves: lets an addon notice an older server. */
57140
+ var MigrationApiInfoSchema = object({
57141
+ version: number().int(),
57142
+ methods: array(string())
57143
+ });
57144
+ object({
57145
+ message: string(),
57146
+ meta: Rows.optional(),
57147
+ /** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
57148
+ tags: object({ deviceId: number().int().optional() }).optional()
57149
+ });
57150
+ object({
57151
+ owner: string().min(1),
57152
+ metas: array(MigrationMetaSchema)
57153
+ }), object({
57154
+ bootId: string(),
57155
+ snapshot: _enum([
57156
+ "none",
57157
+ "written",
57158
+ "refused"
57159
+ ]),
57160
+ steps: array(MigrationOwnerStepSchema),
57161
+ /** The migration API the engine serves; absent from a server that predates it. */
57162
+ migrationApi: MigrationApiInfoSchema.optional()
57163
+ }), object({ id: string().min(1) }), object({
57164
+ proceed: boolean(),
57165
+ state: union([literal("pending"), MigrationStatusSchema]),
57166
+ reason: string().nullable()
57167
+ }), object({
57168
+ id: string().min(1),
57169
+ result: MigrationOwnerResultSchema
57170
+ }), object({
57171
+ status: MigrationStatusSchema,
57172
+ error: string().nullable(),
57173
+ rowsChanged: number().int().nonnegative()
57174
+ }), object({
57175
+ rows: number().int().nonnegative(),
57176
+ secrets: number().int().nonnegative()
57177
+ }), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
57178
+ present: boolean(),
57179
+ removed: Rows
57180
+ }), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
57181
+ ...RecordPutShape,
57182
+ ifRev: number().int().optional()
57183
+ }), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
57184
+ partial: Rows,
57185
+ ifRev: number().int().optional()
57186
+ }), object({ rev: number().int() }), object({ deleted: boolean() }), object({
57187
+ deviceId: number().int().optional(),
57188
+ atFrom: number().optional(),
57189
+ atTo: number().optional(),
57190
+ where: RecordWhereSchema.optional(),
57191
+ limit: number().int().min(1).optional(),
57192
+ cursor: string().min(1).optional(),
57193
+ resolveSecrets: boolean().optional()
57194
+ }), object({
57195
+ records: array(RecordRowSchema),
57196
+ nextCursor: string().nullable()
57197
+ }), object({
57198
+ deviceId: number().int().optional(),
57199
+ atBefore: number().optional(),
57200
+ where: RecordWhereSchema.optional()
57201
+ }), object({ count: number().int().nonnegative() }), object({}), object({ store: Rows }), object({ patch: Rows }), object({}), object({ keys: array(string().min(1)) }), object({ removed: array(string()) }), object({ refs: array(string()) }), object({ values: record(string(), string().nullable()) }), object({
57202
+ ref: string().min(1),
57203
+ value: string()
57204
+ }), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
57205
+ addonId: string().min(1),
57206
+ integrationId: string().min(1).optional()
57207
+ }), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
57208
+ id: number().int(),
57209
+ integrationId: string().nullable(),
57210
+ /** Whether the device carries any stored config; its content is not offered. */
57211
+ hasConfig: boolean()
57212
+ }).nullable(), object({
57213
+ deviceId: number().int(),
57214
+ integrationId: string().min(1)
57215
+ }), object({}), object({}), object({ nodeId: string() }), object({}), object({});
56835
57216
  method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
56836
57217
  id: string().min(1),
56837
57218
  error: string().min(1)
@@ -56861,7 +57242,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56861
57242
  "written",
56862
57243
  "refused"
56863
57244
  ]),
56864
- steps: array(MigrationOwnerStepSchema)
57245
+ steps: array(MigrationOwnerStepSchema),
57246
+ /** The migration API this engine serves (additive: a server that predates it omits it). */
57247
+ migrationApi: MigrationApiInfoSchema.optional()
56865
57248
  }), {
56866
57249
  kind: "mutation",
56867
57250
  auth: "admin"
@@ -56882,6 +57265,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56882
57265
  }), {
56883
57266
  kind: "mutation",
56884
57267
  auth: "admin"
57268
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
57269
+ rows: number().int().nonnegative(),
57270
+ secrets: number().int().nonnegative()
57271
+ }), {
57272
+ kind: "mutation",
57273
+ auth: "admin"
57274
+ }), method(object({
57275
+ integrationId: string().min(1),
57276
+ patch: record(string(), unknown())
57277
+ }), object({ written: boolean() }), {
57278
+ kind: "mutation",
57279
+ auth: "admin"
57280
+ }), method(object({
57281
+ integrationId: string().min(1),
57282
+ keys: array(string().min(1))
57283
+ }), object({
57284
+ present: boolean(),
57285
+ removed: record(string(), unknown())
57286
+ }), {
57287
+ kind: "mutation",
57288
+ auth: "admin"
56885
57289
  });
56886
57290
  method(object({
56887
57291
  sourceUrl: string(),
@@ -59507,7 +59911,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
59507
59911
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
59508
59912
  kind: "mutation",
59509
59913
  auth: "admin"
59510
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59914
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
59915
+ kind: "query",
59916
+ auth: "admin"
59917
+ }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59511
59918
  kind: "query",
59512
59919
  auth: "admin"
59513
59920
  }), method(object({}), array(RelocateJobSchema).readonly(), {
@@ -61914,7 +62321,7 @@ var BrokerStatusSchema$1 = _enum([
61914
62321
  "unreachable",
61915
62322
  "tls-error"
61916
62323
  ]);
61917
- var BrokerInfoSchema$1 = object({
62324
+ var BrokerInfoSchema = object({
61918
62325
  id: string(),
61919
62326
  name: string(),
61920
62327
  url: string(),
@@ -61977,7 +62384,7 @@ var StatusSchema = object({
61977
62384
  brokerCount: number(),
61978
62385
  embeddedRunning: boolean()
61979
62386
  });
61980
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
62387
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
61981
62388
  kind: "mutation",
61982
62389
  auth: "admin"
61983
62390
  }), method(IdInputSchema, _void(), {
@@ -68295,253 +68702,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
68295
68702
  auth: "admin"
68296
68703
  });
68297
68704
  /**
68298
- * records - an addon's own facts and documents (D881): ledgers, audit rows,
68299
- * config documents, mirrors. The third storage API next to `ctx.settings`
68300
- * (operator intent, D870) and device-history (numbers over time, D850).
68301
- *
68302
- * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
68303
- * crossed a process and stated by a hub in-process caller (`ctx.records` does
68304
- * that with the addon id closed over). It is deliberately NOT in any input
68305
- * schema, so a session can never supply it, and a call with no stamp is refused
68306
- * `records/no-caller`. A kind is readable and writable only by its owner; other
68307
- * addons go through the owner's capability.
68308
- *
68309
- * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
68310
- * never `[]`. Secret fields are never on the wire: a read shows the
68311
- * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
68312
- * resolves itself, and a write may carry only a keychain marker bound to the
68313
- * row's own ref.
68314
- *
68315
- * Internal: addons reach it through `ctx.records`, never directly.
68316
- */
68317
- var RecordIdSchema = string().min(1).max(256);
68318
- /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
68319
- var RecordBodySchema = record(string(), unknown());
68320
- var RecordInputSchema = object({
68321
- id: RecordIdSchema,
68322
- /** The device this row belongs to (D865 removal, per-device reads). */
68323
- deviceId: number().int().optional(),
68324
- /** The domain clock, ms: observed / created / opened. */
68325
- at: number().int().optional(),
68326
- /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
68327
- expiresAt: number().int().optional(),
68328
- data: RecordBodySchema
68329
- });
68330
- var RecordViewSchema = object({
68331
- id: string(),
68332
- deviceId: number().int().nullable(),
68333
- at: number().nullable(),
68334
- expiresAt: number().nullable(),
68335
- rev: number().int(),
68336
- updatedAt: number(),
68337
- data: RecordBodySchema
68338
- });
68339
- /** Declared index fields only. Anything else does not compile, by design. */
68340
- var RecordWhereSchema = record(RecordFieldPathSchema, union([
68341
- string(),
68342
- number(),
68343
- boolean(),
68344
- object({ in: array(union([
68345
- string(),
68346
- number(),
68347
- boolean()
68348
- ])).min(1).max(RECORD_BATCH_MAX) }),
68349
- object({
68350
- gt: union([string(), number()]).optional(),
68351
- gte: union([string(), number()]).optional(),
68352
- lt: union([string(), number()]).optional(),
68353
- lte: union([string(), number()]).optional()
68354
- }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
68355
- ]));
68356
- var KindShape = { kind: RecordKindNameSchema };
68357
- var TimeWindowShape = {
68358
- deviceId: number().int().optional(),
68359
- atFrom: number().optional(),
68360
- atTo: number().optional()
68361
- };
68362
- var RecordQueryInputSchema = object({
68363
- ...KindShape,
68364
- ...TimeWindowShape,
68365
- where: RecordWhereSchema.optional(),
68366
- /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
68367
- order: _enum(["at", "id"]).optional(),
68368
- desc: boolean().optional(),
68369
- limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
68370
- /** Opaque keyset cursor from a previous answer. There is no offset. */
68371
- cursor: string().min(1).optional()
68372
- });
68373
- var RecordQueryResultSchema = object({
68374
- records: array(RecordViewSchema),
68375
- nextCursor: string().nullable()
68376
- });
68377
- var RecordKindStateSchema = _enum([
68378
- "created",
68379
- "unchanged",
68380
- "updated"
68381
- ]);
68382
- var DeclareKindResultSchema = object({
68383
- kindId: number().int(),
68384
- state: RecordKindStateSchema,
68385
- createdIndexes: array(string())
68386
- });
68387
- /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
68388
- var SecretRefsList = array(string());
68389
- /**
68390
- * One op of a `transact`: the single-row writes, each on a kind of the caller.
68391
- * All ops of one call share ONE database file and ONE `transactFamily`.
68392
- */
68393
- var RecordTransactOpSchema = discriminatedUnion("op", [
68394
- object({
68395
- op: literal("put"),
68396
- kind: RecordKindNameSchema,
68397
- record: RecordInputSchema,
68398
- ifRev: number().int().min(0).optional()
68399
- }),
68400
- object({
68401
- op: literal("patch"),
68402
- kind: RecordKindNameSchema,
68403
- id: RecordIdSchema,
68404
- data: RecordBodySchema,
68405
- at: number().int().nullable().optional(),
68406
- expiresAt: number().int().nullable().optional(),
68407
- ifRev: number().int().min(1).optional()
68408
- }),
68409
- object({
68410
- op: literal("delete"),
68411
- kind: RecordKindNameSchema,
68412
- id: RecordIdSchema
68413
- })
68414
- ]);
68415
- var RecordSecretRefsSchema = record(string(), record(string(), string()));
68416
- method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
68417
- kind: "mutation",
68418
- auth: "admin"
68419
- }), method(object({
68420
- ...KindShape,
68421
- id: RecordIdSchema
68422
- }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
68423
- ...KindShape,
68424
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68425
- }), array(RecordViewSchema), { auth: "admin" }), method(object({
68426
- ...KindShape,
68427
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68428
- }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
68429
- ...KindShape,
68430
- record: RecordInputSchema,
68431
- /** Compare-and-set. `0` means "the row must not exist". */
68432
- ifRev: number().int().min(0).optional()
68433
- }), object({
68434
- rev: number().int(),
68435
- releasedRefs: SecretRefsList
68436
- }), {
68437
- kind: "mutation",
68438
- auth: "admin"
68439
- }), method(object({
68440
- ...KindShape,
68441
- records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
68442
- }), object({
68443
- count: number().int(),
68444
- releasedRefs: SecretRefsList
68445
- }), {
68446
- kind: "mutation",
68447
- auth: "admin"
68448
- }), method(object({
68449
- ...KindShape,
68450
- id: RecordIdSchema,
68451
- data: RecordBodySchema,
68452
- at: number().int().nullable().optional(),
68453
- expiresAt: number().int().nullable().optional(),
68454
- ifRev: number().int().min(1).optional()
68455
- }), object({
68456
- rev: number().int(),
68457
- releasedRefs: SecretRefsList
68458
- }), {
68459
- kind: "mutation",
68460
- auth: "admin"
68461
- }), method(object({
68462
- ...KindShape,
68463
- id: RecordIdSchema
68464
- }), object({
68465
- deleted: boolean(),
68466
- secretRefs: SecretRefsList
68467
- }), {
68468
- kind: "mutation",
68469
- auth: "admin"
68470
- }), method(object({
68471
- ...KindShape,
68472
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68473
- }), object({
68474
- deleted: number().int(),
68475
- secretRefs: SecretRefsList
68476
- }), {
68477
- kind: "mutation",
68478
- auth: "admin"
68479
- }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
68480
- ...KindShape,
68481
- ...TimeWindowShape,
68482
- where: RecordWhereSchema.optional()
68483
- }), object({ count: number().int() }), { auth: "admin" }), method(object({
68484
- ...KindShape,
68485
- deviceId: number().int().optional(),
68486
- from: number(),
68487
- to: number(),
68488
- bucketMs: number().int().positive()
68489
- }), object({ buckets: array(object({
68490
- bucketStart: number(),
68491
- count: number().int()
68492
- })) }), { auth: "admin" }), method(object({
68493
- ...KindShape,
68494
- op: _enum([
68495
- "sum",
68496
- "min",
68497
- "max",
68498
- "count"
68499
- ]),
68500
- field: RecordFieldPathSchema.optional(),
68501
- groupBy: RecordFieldPathSchema.optional(),
68502
- where: RecordWhereSchema.optional()
68503
- }), object({ rows: array(object({
68504
- group: union([
68505
- string(),
68506
- number(),
68507
- _null()
68508
- ]),
68509
- value: number().nullable()
68510
- })) }), { auth: "admin" }), method(object({
68511
- ...KindShape,
68512
- deviceId: number().int().optional(),
68513
- atBefore: number().optional(),
68514
- where: RecordWhereSchema.optional()
68515
- }), object({
68516
- deleted: number().int(),
68517
- secretRefs: SecretRefsList
68518
- }), {
68519
- kind: "mutation",
68520
- auth: "admin"
68521
- }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
68522
- results: array(object({
68523
- rev: number().int().nullable(),
68524
- deleted: boolean()
68525
- })),
68526
- releasedRefs: SecretRefsList,
68527
- secretRefs: SecretRefsList
68528
- }), {
68529
- kind: "mutation",
68530
- auth: "admin"
68531
- }), method(object({
68532
- deviceId: number().int(),
68533
- kinds: array(RecordKindNameSchema).optional()
68534
- }), object({
68535
- deleted: record(string(), number().int()),
68536
- secretRefs: SecretRefsList
68537
- }), {
68538
- kind: "mutation",
68539
- auth: "admin"
68540
- }), method(object(KindShape), object({ id: number().int() }), {
68541
- kind: "mutation",
68542
- auth: "admin"
68543
- });
68544
- /**
68545
68705
  * record-entries - the ONE admin surface for every list of user-created entries
68546
68706
  * (D881): notification targets, brokers, LLM profiles, anything an addon
68547
68707
  * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
@@ -76056,11 +76216,15 @@ discriminatedUnion("kind", [object({
76056
76216
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
76057
76217
  * near F. A device that is not near one level most of the time has no floor.
76058
76218
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
76059
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
76060
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
76219
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
76220
+ * display waking): it is neither a run nor a pause. Among the remaining
76221
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
76222
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
76223
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
76224
+ * is no run.
76061
76225
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
76062
- * grid a person would type; stop dwell covers the longest pause; start dwell
76063
- * covers the longest standby blip above T.
76226
+ * grid a person would type; stop dwell covers the longest pause and the
76227
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
76064
76228
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
76065
76229
  * run must hold exactly one true interval and nothing may flap outside.
76066
76230
  */
@@ -76071,6 +76235,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
76071
76235
  30 * SECOND_MS;
76072
76236
  60 * SECOND_MS;
76073
76237
  20 * MINUTE_MS$1;
76238
+ 90 * SECOND_MS;
76239
+ 30 * MINUTE_MS$1;
76240
+ 10 * MINUTE_MS$1;
76241
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
76074
76242
  10 * SECOND_MS;
76075
76243
  10 * SECOND_MS;
76076
76244
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -77089,20 +77257,6 @@ Object.freeze({
77089
77257
  access: "create",
77090
77258
  role: "viewer"
77091
77259
  },
77092
- "broker.add": {
77093
- capName: "broker",
77094
- capScope: "system",
77095
- addonId: null,
77096
- access: "create",
77097
- role: "admin"
77098
- },
77099
- "broker.get": {
77100
- capName: "broker",
77101
- capScope: "system",
77102
- addonId: null,
77103
- access: "view",
77104
- role: "admin"
77105
- },
77106
77260
  "broker.getBrokerConfig": {
77107
77261
  capName: "broker",
77108
77262
  capScope: "system",
@@ -77110,20 +77264,6 @@ Object.freeze({
77110
77264
  access: "view",
77111
77265
  role: "admin"
77112
77266
  },
77113
- "broker.getSettings": {
77114
- capName: "broker",
77115
- capScope: "system",
77116
- addonId: null,
77117
- access: "view",
77118
- role: "admin"
77119
- },
77120
- "broker.getSettingsSchema": {
77121
- capName: "broker",
77122
- capScope: "system",
77123
- addonId: null,
77124
- access: "view",
77125
- role: "admin"
77126
- },
77127
77267
  "broker.getState": {
77128
77268
  capName: "broker",
77129
77269
  capScope: "system",
@@ -77138,20 +77278,6 @@ Object.freeze({
77138
77278
  access: "view",
77139
77279
  role: "admin"
77140
77280
  },
77141
- "broker.list": {
77142
- capName: "broker",
77143
- capScope: "system",
77144
- addonId: null,
77145
- access: "view",
77146
- role: "admin"
77147
- },
77148
- "broker.listProviders": {
77149
- capName: "broker",
77150
- capScope: "system",
77151
- addonId: null,
77152
- access: "view",
77153
- role: "admin"
77154
- },
77155
77281
  "broker.publish": {
77156
77282
  capName: "broker",
77157
77283
  capScope: "system",
@@ -77159,20 +77285,6 @@ Object.freeze({
77159
77285
  access: "create",
77160
77286
  role: "admin"
77161
77287
  },
77162
- "broker.remove": {
77163
- capName: "broker",
77164
- capScope: "system",
77165
- addonId: null,
77166
- access: "delete",
77167
- role: "admin"
77168
- },
77169
- "broker.setSettings": {
77170
- capName: "broker",
77171
- capScope: "system",
77172
- addonId: null,
77173
- access: "create",
77174
- role: "admin"
77175
- },
77176
77288
  "broker.subscribe": {
77177
77289
  capName: "broker",
77178
77290
  capScope: "system",
@@ -77180,20 +77292,6 @@ Object.freeze({
77180
77292
  access: "create",
77181
77293
  role: "admin"
77182
77294
  },
77183
- "broker.testConnection": {
77184
- capName: "broker",
77185
- capScope: "system",
77186
- addonId: null,
77187
- access: "create",
77188
- role: "admin"
77189
- },
77190
- "broker.testSettings": {
77191
- capName: "broker",
77192
- capScope: "system",
77193
- addonId: null,
77194
- access: "create",
77195
- role: "admin"
77196
- },
77197
77295
  "broker.unsubscribe": {
77198
77296
  capName: "broker",
77199
77297
  capScope: "system",
@@ -79826,13 +79924,6 @@ Object.freeze({
79826
79924
  access: "view",
79827
79925
  role: "admin"
79828
79926
  },
79829
- "integrations.getSettings": {
79830
- capName: "integrations",
79831
- capScope: "system",
79832
- addonId: null,
79833
- access: "view",
79834
- role: "admin"
79835
- },
79836
79927
  "integrations.list": {
79837
79928
  capName: "integrations",
79838
79929
  capScope: "system",
@@ -79840,13 +79931,6 @@ Object.freeze({
79840
79931
  access: "view",
79841
79932
  role: "admin"
79842
79933
  },
79843
- "integrations.setSettings": {
79844
- capName: "integrations",
79845
- capScope: "system",
79846
- addonId: null,
79847
- access: "create",
79848
- role: "admin"
79849
- },
79850
79934
  "integrations.testConnection": {
79851
79935
  capName: "integrations",
79852
79936
  capScope: "system",
@@ -80841,6 +80925,13 @@ Object.freeze({
80841
80925
  access: "view",
80842
80926
  role: "admin"
80843
80927
  },
80928
+ "migrations.clearLegacyIntegrationRows": {
80929
+ capName: "migrations",
80930
+ capScope: "system",
80931
+ addonId: null,
80932
+ access: "delete",
80933
+ role: "admin"
80934
+ },
80844
80935
  "migrations.declareOwner": {
80845
80936
  capName: "migrations",
80846
80937
  capScope: "system",
@@ -80848,6 +80939,13 @@ Object.freeze({
80848
80939
  access: "create",
80849
80940
  role: "admin"
80850
80941
  },
80942
+ "migrations.deleteLegacyIntegrationRowKeys": {
80943
+ capName: "migrations",
80944
+ capScope: "system",
80945
+ addonId: null,
80946
+ access: "delete",
80947
+ role: "admin"
80948
+ },
80851
80949
  "migrations.dryRun": {
80852
80950
  capName: "migrations",
80853
80951
  capScope: "system",
@@ -80883,6 +80981,20 @@ Object.freeze({
80883
80981
  access: "create",
80884
80982
  role: "admin"
80885
80983
  },
80984
+ "migrations.patchLegacyIntegrationRows": {
80985
+ capName: "migrations",
80986
+ capScope: "system",
80987
+ addonId: null,
80988
+ access: "create",
80989
+ role: "admin"
80990
+ },
80991
+ "migrations.readLegacyIntegrationRows": {
80992
+ capName: "migrations",
80993
+ capScope: "system",
80994
+ addonId: null,
80995
+ access: "view",
80996
+ role: "admin"
80997
+ },
80886
80998
  "migrations.restorePreMigration": {
80887
80999
  capName: "migrations",
80888
81000
  capScope: "system",
@@ -82143,6 +82255,13 @@ Object.freeze({
82143
82255
  access: "view",
82144
82256
  role: "viewer"
82145
82257
  },
82258
+ "pipelineAnalytics.getMediaLocationCensus": {
82259
+ capName: "pipeline-analytics",
82260
+ capScope: "device",
82261
+ addonId: null,
82262
+ access: "view",
82263
+ role: "admin"
82264
+ },
82146
82265
  "pipelineAnalytics.getMediaReclaimStatus": {
82147
82266
  capName: "pipeline-analytics",
82148
82267
  capScope: "device",
@@ -91115,6 +91234,7 @@ object({
91115
91234
  delay: number().nonnegative().optional(),
91116
91235
  commands: record(string(), unknown())
91117
91236
  });
91237
+ entryBody({ brokerId: string() });
91118
91238
  //#endregion
91119
91239
  //#region src/google-types.ts
91120
91240
  /** One Google account connection (broker entry). Credentials for the OAuth
@@ -91200,6 +91320,10 @@ var OAUTH_START_PATH = `/addon/google-services/oauth/start`;
91200
91320
  var OAUTH_CALLBACK_SUFFIX = `/addon/google-services/oauth/callback`;
91201
91321
  //#endregion
91202
91322
  //#region src/google-broker-registry.ts
91323
+ /**
91324
+ * Google broker registry — N Google accounts under kind `google`.
91325
+ * No live WS; "connected" means a refresh token exists for the broker id.
91326
+ */
91203
91327
  var GoogleAddSettingsSchema = object({
91204
91328
  clientId: string().min(1),
91205
91329
  clientSecret: string().min(1).meta({ secret: true }),
@@ -91307,7 +91431,7 @@ function parseStampedCalendarId(stamped) {
91307
91431
  }
91308
91432
  /**
91309
91433
  * In-memory index over broker entries + oauth accounts. The addon owns
91310
- * persistence; this class only allocates ids and builds BrokerInfo rows.
91434
+ * persistence; this class only allocates ids and tracks the OAuth state.
91311
91435
  */
91312
91436
  var GoogleBrokerRegistry = class {
91313
91437
  nextIdCounter = 1;
@@ -91404,28 +91528,6 @@ var GoogleBrokerRegistry = class {
91404
91528
  this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
91405
91529
  this.seedCounterFromEntries(this.entries);
91406
91530
  }
91407
- toBrokerInfo(entry, addonId) {
91408
- const oauth = this.oauthById.get(entry.id);
91409
- const connected = this.isConnected(entry.id);
91410
- const reauth = this.needsReauth(entry.id);
91411
- return {
91412
- id: entry.id,
91413
- addonId,
91414
- name: entry.name,
91415
- kind: GOOGLE_BROKER_KIND,
91416
- status: connected ? "connected" : reauth ? "auth-failed" : "disconnected",
91417
- info: {
91418
- accountEmail: oauth?.accountEmail ?? "",
91419
- hasClient: entry.clientId.length > 0
91420
- },
91421
- lastCheckedAt: connected ? Date.now() : null,
91422
- error: connected ? null : reauth ? "Google session expired — sign in again" : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
91423
- };
91424
- }
91425
- listBrokerInfos(addonId, kind) {
91426
- if (kind !== void 0 && kind !== "google") return [];
91427
- return this.entries.map((e) => this.toBrokerInfo(e, addonId));
91428
- }
91429
91531
  size() {
91430
91532
  return this.entries.length;
91431
91533
  }
@@ -91531,8 +91633,8 @@ function escapeHtml$1(s) {
91531
91633
  //#region src/google-broker-settings-schema.ts
91532
91634
  /**
91533
91635
  * Per-kind broker creation/edit form for kind=`google`.
91534
- * Account linking (Sign in) is driven by `listProviders.interactiveConnectPath`
91535
- * in BrokerManager — not a hard-coded Connect link in this form.
91636
+ * Account linking (Sign in) is driven by the kind's `connectPath`
91637
+ * on the instances page — not a hard-coded Connect link in this form.
91536
91638
  */
91537
91639
  function consoleLinksHtml() {
91538
91640
  return [
@@ -92165,6 +92267,8 @@ function buildGoogleInstances(records, deps) {
92165
92267
  kinds: [{
92166
92268
  id: GOOGLE_BROKER_KIND,
92167
92269
  label: GOOGLE_BROKER_KIND_LABEL,
92270
+ functionCap: "broker",
92271
+ brokerKind: GOOGLE_BROKER_KIND,
92168
92272
  connectPath: deps.connectPath,
92169
92273
  disconnectPath: deps.disconnectPath
92170
92274
  }],
@@ -92589,7 +92693,6 @@ var GoogleServicesAddon = class extends BaseAddon {
92589
92693
  }
92590
92694
  /** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
92591
92695
  buildInstances(records) {
92592
- const brokerProvider = this.buildBrokerProvider();
92593
92696
  return buildGoogleInstances(records, {
92594
92697
  connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
92595
92698
  disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`,
@@ -92602,131 +92705,19 @@ var GoogleServicesAddon = class extends BaseAddon {
92602
92705
  this.registry.seedFrom(this.activeBrokers, accounts);
92603
92706
  await this.persistOauthAccounts();
92604
92707
  },
92605
- testSaved: async (id) => brokerProvider.testConnection({ id }),
92606
- testClient: async (clientId, clientSecret) => brokerProvider.testSettings({
92607
- kind: GOOGLE_BROKER_KIND,
92608
- settings: {
92609
- clientId,
92610
- clientSecret
92611
- }
92612
- })
92708
+ testSaved: (id) => this.probeSavedAccount(id),
92709
+ testClient: (clientId, clientSecret) => this.probeOauthClient(clientId, clientSecret)
92613
92710
  });
92614
92711
  }
92712
+ /**
92713
+ * The `broker` cap is the bus only (D1013): the accounts are the `integration-instances`
92714
+ * provider; calendar and mail go through their own caps, so publish and subscribe are refused.
92715
+ */
92615
92716
  buildBrokerProvider() {
92616
- const ownerAddonId = this.ctx.id;
92617
- const owns = (id) => this.registry.getEntry(id) !== null;
92618
92717
  const notSupported = async () => {
92619
92718
  throw new Error("google-services broker: publish/subscribe/getState are not supported — use calendar-source / smtp-provider");
92620
92719
  };
92621
92720
  return {
92622
- list: async ({ kind }) => this.registry.listBrokerInfos(ownerAddonId, kind),
92623
- get: async ({ id }) => {
92624
- const entry = this.registry.getEntry(id);
92625
- return entry ? this.registry.toBrokerInfo(entry, ownerAddonId) : null;
92626
- },
92627
- listProviders: async () => [{
92628
- addonId: ownerAddonId,
92629
- kinds: [{
92630
- kind: GOOGLE_BROKER_KIND,
92631
- label: GOOGLE_BROKER_KIND_LABEL,
92632
- interactiveConnectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
92633
- interactiveDisconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`
92634
- }]
92635
- }],
92636
- add: async ({ kind, name, settings, id }) => {
92637
- if (kind !== "google") throw new Error(`google-services: only kind '${GOOGLE_BROKER_KIND}' is handled here (got '${kind}')`);
92638
- const entry = this.registry.createEntry(name, settings, id !== void 0 ? { id } : {});
92639
- await this.persistBrokers();
92640
- return { id: entry.id };
92641
- },
92642
- remove: async ({ id }) => {
92643
- if (!owns(id)) return;
92644
- this.registry.removeEntry(id);
92645
- await this.persistBrokers();
92646
- await this.persistOauthAccounts();
92647
- },
92648
- testConnection: async ({ id }) => {
92649
- if (!owns(id)) return {
92650
- ok: false,
92651
- error: "unknown broker"
92652
- };
92653
- const entry = this.registry.getEntry(id);
92654
- if (entry === null) return {
92655
- ok: false,
92656
- error: "unknown broker"
92657
- };
92658
- if (!entry.clientId || !entry.clientSecret) return {
92659
- ok: false,
92660
- error: "Configure Client ID and Client secret first"
92661
- };
92662
- let redirectUri;
92663
- try {
92664
- redirectUri = await this.resolveRedirectUri();
92665
- } catch (err) {
92666
- return {
92667
- ok: false,
92668
- error: err instanceof Error ? err.message : String(err)
92669
- };
92670
- }
92671
- if (!this.registry.isConnected(id)) return probeGoogleOauthClient({
92672
- clientId: entry.clientId,
92673
- clientSecret: entry.clientSecret,
92674
- redirectUri
92675
- });
92676
- try {
92677
- const started = Date.now();
92678
- const token = await this.ensureAccessToken(id, { forceRefresh: true });
92679
- if (token.trim().length === 0) return {
92680
- ok: false,
92681
- error: "No access token after refresh — Sign out and Sign in again"
92682
- };
92683
- let res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${token}` } });
92684
- if (res.status === 401) {
92685
- const retryToken = await this.ensureAccessToken(id, { forceRefresh: true });
92686
- res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${retryToken}` } });
92687
- }
92688
- if (!res.ok) {
92689
- const text = await res.text();
92690
- return {
92691
- ok: false,
92692
- error: `Google Calendar probe failed (${res.status}): ${text.slice(0, 200)}`
92693
- };
92694
- }
92695
- const email = await this.fetchAccountEmail(token);
92696
- if (email.length > 0) {
92697
- const oauth = this.registry.getOauth(id);
92698
- if (oauth !== null && oauth.accountEmail !== email) {
92699
- this.registry.setOauth({
92700
- ...oauth,
92701
- accountEmail: email
92702
- });
92703
- await this.persistOauthAccounts();
92704
- }
92705
- }
92706
- return {
92707
- ok: true,
92708
- latencyMs: Date.now() - started
92709
- };
92710
- } catch (err) {
92711
- return {
92712
- ok: false,
92713
- error: err instanceof Error ? err.message : String(err)
92714
- };
92715
- }
92716
- },
92717
- getSettings: async ({ id }) => {
92718
- const entry = this.registry.getEntry(id);
92719
- if (entry === null) return null;
92720
- return {
92721
- ...brokerEntryToSettings(entry),
92722
- _calendarCatalog: await this.loadStampedCalendarCatalog(id)
92723
- };
92724
- },
92725
- setSettings: async ({ id, settings }) => {
92726
- if (!owns(id)) return;
92727
- this.registry.updateEntry(id, settings);
92728
- await this.persistBrokers();
92729
- },
92730
92721
  getBrokerConfig: async ({ id }) => {
92731
92722
  const entry = this.registry.getEntry(id);
92732
92723
  if (entry === null) return null;
@@ -92738,33 +92729,6 @@ var GoogleServicesAddon = class extends BaseAddon {
92738
92729
  connected: this.registry.isConnected(id)
92739
92730
  };
92740
92731
  },
92741
- getSettingsSchema: async ({ kind }) => {
92742
- if (kind !== "google") return null;
92743
- return buildGoogleBrokerSettingsSchema(this.publicOrigin);
92744
- },
92745
- testSettings: async ({ kind, settings }) => {
92746
- if (kind !== "google") return {
92747
- ok: false,
92748
- error: `unsupported kind: ${kind}`
92749
- };
92750
- try {
92751
- const parsed = object({
92752
- clientId: string().min(1),
92753
- clientSecret: string().min(1).meta({ secret: true })
92754
- }).parse(settings);
92755
- const redirectUri = await this.resolveRedirectUri();
92756
- return probeGoogleOauthClient({
92757
- clientId: parsed.clientId,
92758
- clientSecret: parsed.clientSecret,
92759
- redirectUri
92760
- });
92761
- } catch (err) {
92762
- return {
92763
- ok: false,
92764
- error: err instanceof Error ? err.message : String(err)
92765
- };
92766
- }
92767
- },
92768
92732
  publish: notSupported,
92769
92733
  subscribe: notSupported,
92770
92734
  unsubscribe: notSupported,
@@ -92775,6 +92739,95 @@ var GoogleServicesAddon = class extends BaseAddon {
92775
92739
  })
92776
92740
  };
92777
92741
  }
92742
+ /** Probe a saved account: the OAuth client, then (when signed in) a Calendar call. */
92743
+ async probeSavedAccount(id) {
92744
+ const entry = this.registry.getEntry(id);
92745
+ if (entry === null) return {
92746
+ ok: false,
92747
+ error: "unknown broker"
92748
+ };
92749
+ if (!entry.clientId || !entry.clientSecret) return {
92750
+ ok: false,
92751
+ error: "Configure Client ID and Client secret first"
92752
+ };
92753
+ let redirectUri;
92754
+ try {
92755
+ redirectUri = await this.resolveRedirectUri();
92756
+ } catch (err) {
92757
+ return {
92758
+ ok: false,
92759
+ error: err instanceof Error ? err.message : String(err)
92760
+ };
92761
+ }
92762
+ if (!this.registry.isConnected(id)) return probeGoogleOauthClient({
92763
+ clientId: entry.clientId,
92764
+ clientSecret: entry.clientSecret,
92765
+ redirectUri
92766
+ });
92767
+ try {
92768
+ const started = Date.now();
92769
+ const token = await this.ensureAccessToken(id, { forceRefresh: true });
92770
+ if (token.trim().length === 0) return {
92771
+ ok: false,
92772
+ error: "No access token after refresh — Sign out and Sign in again"
92773
+ };
92774
+ let res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${token}` } });
92775
+ if (res.status === 401) {
92776
+ const retryToken = await this.ensureAccessToken(id, { forceRefresh: true });
92777
+ res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${retryToken}` } });
92778
+ }
92779
+ if (!res.ok) {
92780
+ const text = await res.text();
92781
+ return {
92782
+ ok: false,
92783
+ error: `Google Calendar probe failed (${res.status}): ${text.slice(0, 200)}`
92784
+ };
92785
+ }
92786
+ const email = await this.fetchAccountEmail(token);
92787
+ if (email.length > 0) {
92788
+ const oauth = this.registry.getOauth(id);
92789
+ if (oauth !== null && oauth.accountEmail !== email) {
92790
+ this.registry.setOauth({
92791
+ ...oauth,
92792
+ accountEmail: email
92793
+ });
92794
+ await this.persistOauthAccounts();
92795
+ }
92796
+ }
92797
+ return {
92798
+ ok: true,
92799
+ latencyMs: Date.now() - started
92800
+ };
92801
+ } catch (err) {
92802
+ return {
92803
+ ok: false,
92804
+ error: err instanceof Error ? err.message : String(err)
92805
+ };
92806
+ }
92807
+ }
92808
+ /** Probe an OAuth client that is not saved yet. */
92809
+ async probeOauthClient(clientId, clientSecret) {
92810
+ try {
92811
+ const parsed = object({
92812
+ clientId: string().min(1),
92813
+ clientSecret: string().min(1).meta({ secret: true })
92814
+ }).parse({
92815
+ clientId,
92816
+ clientSecret
92817
+ });
92818
+ const redirectUri = await this.resolveRedirectUri();
92819
+ return probeGoogleOauthClient({
92820
+ clientId: parsed.clientId,
92821
+ clientSecret: parsed.clientSecret,
92822
+ redirectUri
92823
+ });
92824
+ } catch (err) {
92825
+ return {
92826
+ ok: false,
92827
+ error: err instanceof Error ? err.message : String(err)
92828
+ };
92829
+ }
92830
+ }
92778
92831
  async handleStart(req, reply) {
92779
92832
  const brokerIdRaw = req.query["brokerId"];
92780
92833
  const brokerId = typeof brokerIdRaw === "string" ? brokerIdRaw.trim() : "";