@camstack/addon-smtp-nodemailer 1.2.148 → 1.2.149

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.
@@ -6018,7 +6018,7 @@ var ZodIssueCode = {
6018
6018
  var ZodFirstPartyTypeKind;
6019
6019
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6020
6020
  //#endregion
6021
- //#region ../types/dist/sleep-DEZaLbiu.mjs
6021
+ //#region ../types/dist/sleep-BX_MqzUe.mjs
6022
6022
  /**
6023
6023
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6024
6024
  * window to float samples (D455).
@@ -7647,6 +7647,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
7647
7647
  /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
7648
7648
  var SECRET_CHANGE_DEBOUNCE_MS = 250;
7649
7649
  /**
7650
+ * Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
7651
+ * place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
7652
+ * through `api` (guard `check-migration-ports-surface`).
7653
+ */
7654
+ function migrationSet(catalogue, bindPorts) {
7655
+ return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
7656
+ }
7657
+ /** A mixed array is a mistake: the other kind would be dropped without a word. */
7658
+ function refuseMixed(form, kept, dropped) {
7659
+ 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`);
7660
+ }
7661
+ function catalogueOf(list) {
7662
+ const kept = list.filter((entry) => !("execute" in entry));
7663
+ refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
7664
+ return kept;
7665
+ }
7666
+ function boundOf(list) {
7667
+ const kept = list.filter((entry) => "execute" in entry);
7668
+ refuseMixed("bound migrations", kept.length, list.length - kept.length);
7669
+ return kept;
7670
+ }
7671
+ /**
7650
7672
  * Base class for CamStack addons. Eliminates settings boilerplate:
7651
7673
  *
7652
7674
  * - Typed `config` property with automatic resolution from store + defaults
@@ -7813,23 +7835,21 @@ var BaseAddon = class {
7813
7835
  await this.loadScopedSchemas();
7814
7836
  return normalized;
7815
7837
  }
7838
+ async runMigrations(first, second, third) {
7839
+ if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
7840
+ return this.runBound(boundOf(first), second);
7841
+ }
7816
7842
  /**
7817
- * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7818
- * engine. Call it at the point of the boot where the data must already have the new
7819
- * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7820
- * restored devices — with the catalogue bound to the addon's ports
7821
- * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7822
- *
7823
- * An engine that cannot be reached or refuses the catalogue is logged and runs
7824
- * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7825
- * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7826
- * serve data whose shape it cannot trust; `tolerate` migrations never do.
7827
- *
7828
- * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7829
- * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7830
- * omitted applied ids "newer").
7843
+ * Several catalogues of ONE owner in one declaration (the engine replaces an owner's
7844
+ * catalogue on every declare, so two calls would each judge the other's ids "newer").
7845
+ * Build each entry with `migrationSet(catalogue, (api) => ports)`.
7831
7846
  */
7832
- async runMigrations(migrations, options) {
7847
+ async runMigrationSets(sets, options) {
7848
+ const api = this.ctx.migrationApi;
7849
+ if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
7850
+ return this.runBound(sets.flatMap((set) => set.bind(api)), options);
7851
+ }
7852
+ async runBound(migrations, options) {
7833
7853
  const api = this.ctx.api.migrations;
7834
7854
  const report = await runAddonMigrations({
7835
7855
  declareOwner: (input) => api.declareOwner.mutate(input),
@@ -13565,26 +13585,16 @@ var RecordIntegrationInstances = class {
13565
13585
  ...this.hooks.description,
13566
13586
  mintsId: true
13567
13587
  };
13568
- /**
13569
- * The record an id names. A caller holding the `integrations` registry id (the Integrations
13570
- * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13571
- * record id answers as it is. An id that names nothing comes back unchanged.
13572
- */
13573
- async resolveRecordId(id) {
13574
- if (await this.handle.get(id) !== null) return id;
13575
- const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13576
- return linked === void 0 ? id : linked.id;
13577
- }
13578
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13588
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13579
13589
  listInstances = async () => {
13580
13590
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13581
13591
  };
13582
13592
  getInstance = async ({ id }) => {
13583
- const record = await this.handle.get(await this.resolveRecordId(id));
13593
+ const record = await this.handle.get(id);
13584
13594
  return record === null ? null : this.viewOf(record);
13585
13595
  };
13586
13596
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13587
- createInstance = async ({ id, name, kind }) => {
13597
+ createInstance = async ({ id, name, kind, values }) => {
13588
13598
  const minted = id ?? this.hooks.mintId?.({
13589
13599
  name,
13590
13600
  ...kind !== void 0 ? { kind } : {}
@@ -13594,11 +13604,20 @@ var RecordIntegrationInstances = class {
13594
13604
  id: minted,
13595
13605
  data: this.hooks.initialBody(name, kind)
13596
13606
  }, { ifRev: 0 });
13607
+ const patch = this.patchOf(values ?? {});
13608
+ if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
13597
13609
  await this.hooks.onChanged?.(minted, "created");
13598
13610
  return { id: minted };
13599
13611
  };
13600
- updateInstance = async ({ id: askedId, name, values }) => {
13601
- const id = await this.resolveRecordId(askedId);
13612
+ updateInstance = async ({ id, name, values }) => {
13613
+ const patch = this.patchOf(values);
13614
+ if (name !== void 0) patch[this.nameField] = name;
13615
+ await this.handle.patch(id, patch);
13616
+ await this.hooks.onChanged?.(id, "updated");
13617
+ return { ok: true };
13618
+ };
13619
+ /** The record patch a form `values` object means: secrets kept, cleared or replaced. */
13620
+ patchOf(values) {
13602
13621
  const patch = {};
13603
13622
  for (const [key, value] of Object.entries(values)) {
13604
13623
  if (value === void 0) continue;
@@ -13609,11 +13628,8 @@ var RecordIntegrationInstances = class {
13609
13628
  else if (isSecretClear(value)) patch[key] = null;
13610
13629
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13611
13630
  }
13612
- if (name !== void 0) patch[this.nameField] = name;
13613
- await this.handle.patch(id, patch);
13614
- await this.hooks.onChanged?.(id, "updated");
13615
- return { ok: true };
13616
- };
13631
+ return patch;
13632
+ }
13617
13633
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13618
13634
  setInstanceEnabled = async ({ id, enabled }) => {
13619
13635
  if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
@@ -13633,10 +13649,7 @@ var RecordIntegrationInstances = class {
13633
13649
  status: "unsupported",
13634
13650
  error: "This integration declares no connection test"
13635
13651
  };
13636
- return this.hooks.test(input.id === null ? input : {
13637
- ...input,
13638
- id: await this.resolveRecordId(input.id)
13639
- });
13652
+ return this.hooks.test(input);
13640
13653
  };
13641
13654
  /** The flat provider object to hand to `registerProvider`. */
13642
13655
  toProvider() {
@@ -13653,6 +13666,76 @@ var RecordIntegrationInstances = class {
13653
13666
  };
13654
13667
  }
13655
13668
  };
13669
+ /** No part holds the id a call named. */
13670
+ var UnknownInstanceError = class extends Error {
13671
+ instanceId;
13672
+ constructor(instanceId) {
13673
+ super(`Unknown instance '${instanceId}'`);
13674
+ this.instanceId = instanceId;
13675
+ this.name = "UnknownInstanceError";
13676
+ }
13677
+ };
13678
+ /** A create or a form named no kind, or a kind no part serves. */
13679
+ var UnknownInstanceKindError = class extends Error {
13680
+ kindId;
13681
+ constructor(kindId) {
13682
+ super(kindId === void 0 ? "This provider has several kinds: name one" : `Unknown instance kind '${kindId}'`);
13683
+ this.kindId = kindId;
13684
+ this.name = "UnknownInstanceKindError";
13685
+ }
13686
+ };
13687
+ function composeInstanceKinds(parts, base) {
13688
+ const byKind = (kindId) => {
13689
+ if (kindId === void 0) {
13690
+ const only = parts.length === 1 ? parts[0] : void 0;
13691
+ if (only === void 0) throw new UnknownInstanceKindError(void 0);
13692
+ return only;
13693
+ }
13694
+ const part = parts.find((candidate) => candidate.kind.id === kindId);
13695
+ if (part === void 0) throw new UnknownInstanceKindError(kindId);
13696
+ return part;
13697
+ };
13698
+ const holderOf = async (id) => {
13699
+ for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13700
+ throw new UnknownInstanceError(id);
13701
+ };
13702
+ const tagged = (part, instance) => ({
13703
+ ...instance,
13704
+ kind: part.kind.id
13705
+ });
13706
+ return {
13707
+ describe: async () => ({
13708
+ ...base,
13709
+ kinds: parts.map((part) => part.kind)
13710
+ }),
13711
+ instanceFormSchema: async ({ id, kind }) => {
13712
+ return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
13713
+ },
13714
+ listInstances: async () => {
13715
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
13716
+ },
13717
+ getInstance: async ({ id }) => {
13718
+ for (const part of parts) {
13719
+ const found = await part.provider.getInstance({ id });
13720
+ if (found !== null) return tagged(part, found);
13721
+ }
13722
+ return null;
13723
+ },
13724
+ createInstance: async (input) => byKind(input.kind).provider.createInstance(input),
13725
+ updateInstance: async (input) => (await holderOf(input.id)).provider.updateInstance(input),
13726
+ setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13727
+ deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13728
+ testInstance: async (input) => {
13729
+ const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
13730
+ if (test === void 0) return {
13731
+ success: false,
13732
+ status: "inconclusive",
13733
+ error: "This kind cannot be tested"
13734
+ };
13735
+ return test(input);
13736
+ }
13737
+ };
13738
+ }
13656
13739
  var EncodeProfileSchema = object({
13657
13740
  video: object({
13658
13741
  codec: _enum([
@@ -15825,6 +15908,39 @@ var UnstampedEventMediaCountSchema = object({
15825
15908
  /** Sum across both, or `null` when either lane could not be counted. */
15826
15909
  total: number().int().nonnegative().nullable()
15827
15910
  }).nullable();
15911
+ /**
15912
+ * Where the stored event media sits, per storage location: how many media rows
15913
+ * and how many bytes carry each location id, how many carry none (legacy,
15914
+ * resolved to the default `eventMedia` location), and how many name a location
15915
+ * that was not asked about (a deleted or never-listed one).
15916
+ *
15917
+ * One covering-index aggregate per asked location plus the NULL one, and one
15918
+ * unfiltered total that scans the covering index: O(rows), not a walk of the
15919
+ * rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
15920
+ * it is a deleted location only when every location id was asked.
15921
+ * `null` is "could not be measured", never zero.
15922
+ */
15923
+ var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
15924
+ var MediaLocationCensusSchema = object({
15925
+ locations: array(object({
15926
+ locationId: string(),
15927
+ rows: number().int().nonnegative(),
15928
+ bytes: number().nonnegative()
15929
+ })).readonly(),
15930
+ unstamped: object({
15931
+ rows: number().int().nonnegative(),
15932
+ bytes: number().nonnegative()
15933
+ }),
15934
+ /** Rows stamped with a location id that was not in the asked list. */
15935
+ unlisted: object({
15936
+ rows: number().int().nonnegative(),
15937
+ bytes: number().nonnegative()
15938
+ }),
15939
+ total: object({
15940
+ rows: number().int().nonnegative(),
15941
+ bytes: number().nonnegative()
15942
+ })
15943
+ });
15828
15944
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
15829
15945
  /** The independently selectable logical storage classes — every class
15830
15946
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -46345,52 +46461,7 @@ DeviceType.Light, method(object({
46345
46461
  level: number().min(0).max(100),
46346
46462
  lastChangedAt: number()
46347
46463
  });
46348
- /**
46349
- * `broker` — unified pub/sub broker registry, system-scoped collection.
46350
- *
46351
- * The cap models any kind-tagged message broker the user wants to
46352
- * register and that other addons might consume. The first two kinds
46353
- * are MQTT (mosquitto / aedes embedded / cloud bridge) and
46354
- * Home Assistant (HA WebSocket — subscribe_entities + call_service).
46355
- * Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
46356
- * the same surface.
46357
- *
46358
- * Why one cap, not one cap per kind:
46359
- * - The integrations page wants a single table of "every broker"
46360
- * across kinds; a unified cap drives it without join logic.
46361
- * - Generic admin operations (add / remove / test / status) are
46362
- * identical across kinds — duplicating them per cap was busywork.
46363
- * - The `network-access` cap precedent: same interface, many
46364
- * `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
46365
- *
46366
- * What stays kind-specific:
46367
- * - `getBrokerConfig` payload (kind decides whether it returns
46368
- * mqtt URL+creds, HA baseUrl+token, …) — typed as
46369
- * `Record<string, unknown>` at the wire; consumers narrow.
46370
- * - `publish` / `subscribe` / `getState` arguments (target / filter /
46371
- * key) — also typed as `Record<string, unknown>` so each kind can
46372
- * evolve its surface (MQTT topic+qos vs HA entity_id+domain)
46373
- * without rebreaking the cap signature.
46374
- * - `add` `settings` payload (different fields per kind, validated by
46375
- * the kind-specific provider).
46376
- *
46377
- * Bidirectionality:
46378
- * - `publish` is RPC — synchronous result (provider-defined).
46379
- * - `subscribe` returns a subscription handle; the actual message
46380
- * stream flows over the typed event-bus as `broker.message` events
46381
- * keyed by `(brokerId, subscriptionId)`. Consumers filter in their
46382
- * event handler. Subscriptions persist across reconnects — the
46383
- * provider re-subscribes upstream after a transport drop.
46384
- *
46385
- * Why event-bus push (not a subscription RPC stream):
46386
- * - Matches the rest of CamStack's D8 contract — events for telemetry,
46387
- * RPC for loss-is-a-bug. Broker messages are telemetry (a single
46388
- * drop is recoverable via `getState({key})`).
46389
- * - Frees `broker.subscribe` from holding a long-lived RPC channel.
46390
- * - Cross-process: events already route through `$event-bus`; no
46391
- * per-broker wire-shaping needed.
46392
- */
46393
- var BrokerStatusEnum = _enum([
46464
+ _enum([
46394
46465
  "connected",
46395
46466
  "disconnected",
46396
46467
  "connecting",
@@ -46398,141 +46469,12 @@ var BrokerStatusEnum = _enum([
46398
46469
  "unreachable",
46399
46470
  "error"
46400
46471
  ]);
46401
- var BrokerInfoSchema = object({
46402
- /** Stable broker id. Persisted; survives addon restarts. */
46403
- id: string(),
46404
- /** Addon id of the provider that OWNS this broker.
46405
- *
46406
- * The `broker` cap is a system-scoped collection: several addons
46407
- * register a `broker` provider, each owning a DISJOINT set of
46408
- * brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
46409
- * `ha_*`). A broker therefore belongs to exactly one addon — like a
46410
- * device belongs to one integration. The admin UI threads this id
46411
- * back as the `{ addonId }` system-collection selector on every
46412
- * id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
46413
- * `testConnection`), so the call routes to the OWNING provider
46414
- * instead of defaulting to the first-registered one. */
46415
- addonId: string(),
46416
- /** Human-readable name (operator-chosen at add-time). */
46417
- name: string(),
46418
- /** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
46419
- kind: string(),
46420
- status: BrokerStatusEnum,
46421
- /** Free-form provider-specific info (HA version, MQTT broker
46422
- * flavour, latency, mTLS active, …). */
46423
- info: record(string(), unknown()),
46424
- /** Ms epoch of the last connection probe / status update. */
46425
- lastCheckedAt: number().nullable(),
46426
- /** Last error message, when `status` indicates failure. */
46427
- error: string().nullable()
46428
- });
46429
46472
  var RegistryStatusSchema = object({
46430
46473
  brokerCount: number().int().nonnegative(),
46431
46474
  connectedCount: number().int().nonnegative()
46432
46475
  });
46433
- /**
46434
- * One entry per `broker` provider — which addon provides which broker
46435
- * kind(s). The unified create picker reads this to know, for each
46436
- * (addonId, kind) pair the operator can pick, where to route the
46437
- * follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
46438
- * `{ addonId }` selector). A provider only advertises a kind here when
46439
- * it implements `getSettingsSchema` for that kind (so the picker never
46440
- * offers a kind whose creation form can't be rendered).
46441
- */
46442
- var BrokerProviderInfoSchema = object({
46443
- /** Addon id of the `broker` provider this entry describes. */
46444
- addonId: string(),
46445
- /** Broker kinds this provider can create, with a display label. */
46446
- kinds: array(object({
46447
- kind: string(),
46448
- label: string(),
46449
- /**
46450
- * Relative hub path to start an interactive account link (OAuth popup).
46451
- * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
46452
- */
46453
- interactiveConnectPath: string().min(1).optional(),
46454
- /** Relative hub path to disconnect the linked account (same popup pattern). */
46455
- interactiveDisconnectPath: string().min(1).optional(),
46456
- /**
46457
- * The kind signs in through a server-held interactive session instead of
46458
- * (or before) typed credentials. `basePath` is a hub-relative route set
46459
- * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
46460
- * contract: `POST {basePath}/start` (body: the form's settings) answers
46461
- * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
46462
- * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
46463
- * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
46464
- * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
46465
- * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
46466
- * (`{ sessionId }`) drops the session. The dialog that edits a broker
46467
- * offers it as the default path; the fields the session fills are
46468
- * marked `filledBySignIn` in the kind's settings schema.
46469
- *
46470
- * `methods` lists the ways the session can sign in, recommended first:
46471
- * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
46472
- * settings) answering `{ sessionId, userCode, verificationUrl,
46473
- * expiresAt }` — the operator types `userCode` at `verificationUrl` on
46474
- * any device, and the same `status` / `commit` / `cancel` routes follow
46475
- * (status then carries `method`, `userCode`, `verificationUrl`,
46476
- * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
46477
- * `'proxy'` is the `{basePath}/start` flow above. Absent means
46478
- * `['proxy']`.
46479
- */
46480
- signIn: object({
46481
- basePath: string().min(1),
46482
- methods: array(_enum(["device-code", "proxy"])).optional()
46483
- }).optional()
46484
- }))
46485
- });
46486
- var ListInputSchema = object({
46487
- /** Optional kind filter — `list({kind:'home-assistant'})` returns
46488
- * only HA brokers. Omit to list every broker. */
46489
- kind: string().optional() });
46490
46476
  var GetInputSchema = object({ id: string() });
46491
- var AddInputSchema = object({
46492
- kind: string().min(1),
46493
- name: string().min(1),
46494
- /**
46495
- * ADOPT an existing id instead of minting a new one.
46496
- *
46497
- * Written the day this cost an outage. A broker's id is not a detail: HA
46498
- * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
46499
- * lost from config and re-added as `ha_001` leaves every one of its devices
46500
- * bound to a broker that no longer exists. Re-entering the password under the
46501
- * ORIGINAL id turns a multi-step device migration back into re-entering a
46502
- * password.
46503
- *
46504
- * A provider MUST refuse an id that is already in use — adopting a live
46505
- * broker's id would silently take it over.
46506
- */
46507
- id: string().min(1).optional(),
46508
- /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
46509
- * `{baseUrl,accessToken}`). Validated by the kind-specific provider
46510
- * branch on receipt — invalid shape rejects the add. */
46511
- settings: record(string(), unknown())
46512
- });
46513
- var AddResultSchema = object({ id: string() });
46514
- var RemoveInputSchema = object({ id: string() });
46515
- var TestConnectionResultSchema = discriminatedUnion("ok", [object({
46516
- ok: literal(true),
46517
- latencyMs: number().nonnegative()
46518
- }), object({
46519
- ok: literal(false),
46520
- error: string()
46521
- })]);
46522
46477
  var SettingsRecordSchema$1 = record(string(), unknown());
46523
- var SettingsSchemaInputSchema = object({ kind: string() });
46524
- var TestSettingsInputSchema = object({
46525
- kind: string(),
46526
- settings: SettingsRecordSchema$1
46527
- });
46528
- var TestSettingsResultSchema = discriminatedUnion("ok", [object({
46529
- ok: literal(true),
46530
- latencyMs: number().nonnegative().optional()
46531
- }).strict(), object({
46532
- ok: literal(false),
46533
- error: string()
46534
- })]);
46535
- var SettingsSchemaResultSchema = unknown().nullable();
46536
46478
  var PublishInputSchema = object({
46537
46479
  brokerId: string(),
46538
46480
  /** Kind-specific routing target.
@@ -46576,51 +46518,12 @@ var brokerCapability = {
46576
46518
  kind: "poll"
46577
46519
  },
46578
46520
  methods: {
46579
- list: method(ListInputSchema, array(BrokerInfoSchema)),
46580
- get: method(GetInputSchema, BrokerInfoSchema.nullable()),
46581
- /** Enumerate which addon provides which broker kind(s) for the
46582
- * unified create picker. The auto-mount fans this array across
46583
- * every registered `broker` provider (array-output method), so the
46584
- * picker sees every kind from every provider in one call. */
46585
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
46586
- add: method(AddInputSchema, AddResultSchema, {
46587
- kind: "mutation",
46588
- auth: "admin"
46589
- }),
46590
- remove: method(RemoveInputSchema, _void(), {
46591
- kind: "mutation",
46592
- auth: "admin"
46593
- }),
46594
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
46595
- kind: "mutation",
46596
- auth: "admin"
46597
- }),
46598
- /** Read the persisted settings record for a broker (kind-specific
46599
- * shape). Admin-only — settings may contain secrets. Returns `null`
46600
- * when the broker id is unknown to the provider (the collection
46601
- * fallback may route a foreign id to the first provider). */
46602
- getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46603
- /** Overwrite the persisted settings record. The kind-specific
46604
- * provider validates the shape and applies the change (reconnects
46605
- * if credentials changed). */
46606
- setSettings: method(object({
46607
- id: string(),
46608
- settings: SettingsRecordSchema$1
46609
- }), _void(), {
46610
- kind: "mutation",
46611
- auth: "admin"
46612
- }),
46613
46521
  /** Returns the kind-specific connection config the consumer needs
46614
46522
  * to open its own client (MQTT pattern: `{url, username, password,
46615
46523
  * clientIdPrefix}`). HA providers MAY return the auth envelope
46616
46524
  * but typical HA consumers use `publish` / `subscribe` instead.
46617
46525
  * Returns `null` when the broker id is unknown to the provider. */
46618
46526
  getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46619
- getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
46620
- testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
46621
- kind: "mutation",
46622
- auth: "admin"
46623
- }),
46624
46527
  publish: method(PublishInputSchema, unknown(), {
46625
46528
  kind: "mutation",
46626
46529
  auth: "admin"
@@ -50693,7 +50596,14 @@ var DiscoveryCandidateSchema = object({
50693
50596
  /** `stableId` of the gateway candidate this device was found BEHIND. */
50694
50597
  parentStableId: string().optional(),
50695
50598
  /** `false` = shown for context but not pickable. Default true. */
50696
- selectable: boolean().optional()
50599
+ selectable: boolean().optional(),
50600
+ /**
50601
+ * Keys of the provider's creation form (`getChildCreationSchema`) this candidate
50602
+ * cannot be adopted without — a key for an encrypted board, a pairing code. The
50603
+ * admin asks for exactly these, on the creation form prefilled with
50604
+ * `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
50605
+ */
50606
+ needsParams: array(string()).optional()
50697
50607
  });
50698
50608
  /**
50699
50609
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -50752,12 +50662,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
50752
50662
  }), array(DiscoveryCandidateSchema), {
50753
50663
  kind: "mutation",
50754
50664
  auth: "admin"
50755
- }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
50665
+ }), method(object({}), CreationSchemaOutputSchema), method(object({
50666
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50667
+ integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
50668
+ candidate: DiscoveryCandidateSchema,
50669
+ /** The integration the operator adopts into; a provider with several instances needs it. */
50670
+ integrationId: string().optional()
50671
+ }), DeviceSummarySchema, {
50756
50672
  kind: "mutation",
50757
50673
  auth: "admin"
50758
- }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
50674
+ }), method(object({}), boolean()), method(object({
50759
50675
  type: _enum(DeviceType),
50760
- config: record(string(), unknown())
50676
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50677
+ integrationId: string().optional()
50678
+ }), CreationSchemaOutputSchema), method(object({
50679
+ type: _enum(DeviceType),
50680
+ config: record(string(), unknown()),
50681
+ /** The integration the operator adds the device to; a provider with several instances needs it. */
50682
+ integrationId: string().optional()
50761
50683
  }), DeviceSummarySchema, {
50762
50684
  kind: "mutation",
50763
50685
  auth: "admin"
@@ -51467,7 +51389,10 @@ method(object({
51467
51389
  auth: "admin"
51468
51390
  }), method(object({
51469
51391
  addonId: string(),
51470
- type: _enum(DeviceType)
51392
+ type: _enum(DeviceType),
51393
+ /** The integration the form is for; a provider whose integrations differ (a hub
51394
+ * integration takes no manual add) answers per integration. Absent = the addon's own. */
51395
+ integrationId: string().optional()
51471
51396
  }), unknown().nullable()), method(object({
51472
51397
  addonId: string(),
51473
51398
  type: _enum(DeviceType),
@@ -51551,7 +51476,11 @@ method(object({
51551
51476
  kind: "mutation",
51552
51477
  auth: "admin",
51553
51478
  timeoutMs: 12e4
51554
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51479
+ }), method(object({
51480
+ addonId: string(),
51481
+ /** The integration the Add-Device dialog is open on; absent = the addon's own. */
51482
+ integrationId: string().optional()
51483
+ }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51555
51484
  deviceId: number(),
51556
51485
  key: string(),
51557
51486
  value: unknown()
@@ -54147,7 +54076,7 @@ var TestConnectionStatusEnum = _enum([
54147
54076
  "inconclusive",
54148
54077
  "unsupported"
54149
54078
  ]);
54150
- var TestConnectionResultSchema$1 = object({
54079
+ var TestConnectionResultSchema = object({
54151
54080
  /** True ONLY for `validated`. Never true for a test that did not run. */
54152
54081
  success: boolean(),
54153
54082
  error: string().optional(),
@@ -54192,7 +54121,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54192
54121
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54193
54122
  addonId: string(),
54194
54123
  settings: record(string(), unknown())
54195
- }), TestConnectionResultSchema$1, {
54124
+ }), TestConnectionResultSchema, {
54196
54125
  kind: "mutation",
54197
54126
  auth: "admin"
54198
54127
  });
@@ -54219,6 +54148,14 @@ var ConfigSchemaPassthrough$3 = custom();
54219
54148
  var IntegrationInstanceIdSchema = string().min(1);
54220
54149
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54221
54150
  /**
54151
+ * What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
54152
+ * instance is an integration that brings devices (the registry owns its on/off switch);
54153
+ * `services`: a connection configured from the addon's own function page, never listed on
54154
+ * Integrations.
54155
+ * It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
54156
+ */
54157
+ var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
54158
+ /**
54222
54159
  * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54223
54160
  * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54224
54161
  * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
@@ -54229,6 +54166,22 @@ var IntegrationInstanceKindSchema = object({
54229
54166
  /** Stable id of the kind, sent back as `createInstance.kind`. */
54230
54167
  id: string().min(1),
54231
54168
  label: string().min(1),
54169
+ /**
54170
+ * The function capability this kind is configured for (`broker`, `smtp-provider`,
54171
+ * `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
54172
+ * provider with more than one section must name it on every kind.
54173
+ */
54174
+ functionCap: string().min(1),
54175
+ /**
54176
+ * For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
54177
+ * (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
54178
+ * finds the instances whose kind names it; absent takes the kind `id`.
54179
+ */
54180
+ brokerKind: string().min(1).optional(),
54181
+ /** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
54182
+ role: IntegrationInstanceRoleSchema.optional(),
54183
+ /** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
54184
+ canTest: boolean().optional(),
54232
54185
  /** The kind signs in through a server-held interactive session. */
54233
54186
  signIn: object({
54234
54187
  basePath: string().min(1),
@@ -54241,11 +54194,7 @@ var IntegrationInstanceKindSchema = object({
54241
54194
  });
54242
54195
  var IntegrationInstanceDescriptionSchema = object({
54243
54196
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54244
- role: _enum([
54245
- "devices",
54246
- "services",
54247
- "both"
54248
- ]),
54197
+ role: IntegrationInstanceRoleSchema,
54249
54198
  /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54250
54199
  discovery: object({
54251
54200
  mode: _enum([
@@ -54320,7 +54269,11 @@ var integrationInstancesCapability = {
54320
54269
  methods: {
54321
54270
  describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
54322
54271
  /** The form; `id: null` is the create form. */
54323
- instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54272
+ instanceFormSchema: method(object({
54273
+ id: IntegrationInstanceIdSchema.nullable(),
54274
+ /** The kind of the create form (`id: null`) of a provider with several. */
54275
+ kind: string().min(1).optional()
54276
+ }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54324
54277
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54325
54278
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54326
54279
  /**
@@ -54330,7 +54283,9 @@ var integrationInstancesCapability = {
54330
54283
  createInstance: method(object({
54331
54284
  id: IntegrationInstanceIdSchema.optional(),
54332
54285
  name: string().min(1),
54333
- kind: string().min(1).optional()
54286
+ kind: string().min(1).optional(),
54287
+ /** Form values written with the record (a link picked in the create dialog). */
54288
+ values: record(string(), unknown()).optional()
54334
54289
  }), object({ id: string() }), {
54335
54290
  kind: "mutation",
54336
54291
  auth: "admin"
@@ -54357,8 +54312,10 @@ var integrationInstancesCapability = {
54357
54312
  }),
54358
54313
  testInstance: optionalMethod(object({
54359
54314
  id: IntegrationInstanceIdSchema.nullable(),
54360
- values: record(string(), unknown())
54361
- }), TestConnectionResultSchema$1, {
54315
+ values: record(string(), unknown()),
54316
+ /** The kind to test when `id` is null and the provider has several. */
54317
+ kind: string().min(1).optional()
54318
+ }), TestConnectionResultSchema, {
54362
54319
  kind: "mutation",
54363
54320
  auth: "admin"
54364
54321
  })
@@ -56863,6 +56820,380 @@ var SystemMetricsSchema = object({
56863
56820
  gpuMemoryPercent: number().optional()
56864
56821
  });
56865
56822
  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);
56823
+ /**
56824
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
56825
+ * config documents, mirrors. The third storage API next to `ctx.settings`
56826
+ * (operator intent, D870) and device-history (numbers over time, D850).
56827
+ *
56828
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
56829
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
56830
+ * that with the addon id closed over). It is deliberately NOT in any input
56831
+ * schema, so a session can never supply it, and a call with no stamp is refused
56832
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
56833
+ * addons go through the owner's capability.
56834
+ *
56835
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
56836
+ * never `[]`. Secret fields are never on the wire: a read shows the
56837
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
56838
+ * resolves itself, and a write may carry only a keychain marker bound to the
56839
+ * row's own ref.
56840
+ *
56841
+ * Internal: addons reach it through `ctx.records`, never directly.
56842
+ */
56843
+ var RecordIdSchema = string().min(1).max(256);
56844
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
56845
+ var RecordBodySchema = record(string(), unknown());
56846
+ var RecordInputSchema = object({
56847
+ id: RecordIdSchema,
56848
+ /** The device this row belongs to (D865 removal, per-device reads). */
56849
+ deviceId: number().int().optional(),
56850
+ /** The domain clock, ms: observed / created / opened. */
56851
+ at: number().int().optional(),
56852
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
56853
+ expiresAt: number().int().optional(),
56854
+ data: RecordBodySchema
56855
+ });
56856
+ var RecordViewSchema = object({
56857
+ id: string(),
56858
+ deviceId: number().int().nullable(),
56859
+ at: number().nullable(),
56860
+ expiresAt: number().nullable(),
56861
+ rev: number().int(),
56862
+ updatedAt: number(),
56863
+ data: RecordBodySchema
56864
+ });
56865
+ /** Declared index fields only. Anything else does not compile, by design. */
56866
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
56867
+ string(),
56868
+ number(),
56869
+ boolean(),
56870
+ object({ in: array(union([
56871
+ string(),
56872
+ number(),
56873
+ boolean()
56874
+ ])).min(1).max(RECORD_BATCH_MAX) }),
56875
+ object({
56876
+ gt: union([string(), number()]).optional(),
56877
+ gte: union([string(), number()]).optional(),
56878
+ lt: union([string(), number()]).optional(),
56879
+ lte: union([string(), number()]).optional()
56880
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
56881
+ ]));
56882
+ var KindShape = { kind: RecordKindNameSchema };
56883
+ var TimeWindowShape = {
56884
+ deviceId: number().int().optional(),
56885
+ atFrom: number().optional(),
56886
+ atTo: number().optional()
56887
+ };
56888
+ var RecordQueryInputSchema = object({
56889
+ ...KindShape,
56890
+ ...TimeWindowShape,
56891
+ where: RecordWhereSchema.optional(),
56892
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
56893
+ order: _enum(["at", "id"]).optional(),
56894
+ desc: boolean().optional(),
56895
+ limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
56896
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
56897
+ cursor: string().min(1).optional()
56898
+ });
56899
+ var RecordQueryResultSchema = object({
56900
+ records: array(RecordViewSchema),
56901
+ nextCursor: string().nullable()
56902
+ });
56903
+ var RecordKindStateSchema = _enum([
56904
+ "created",
56905
+ "unchanged",
56906
+ "updated"
56907
+ ]);
56908
+ var DeclareKindResultSchema = object({
56909
+ kindId: number().int(),
56910
+ state: RecordKindStateSchema,
56911
+ createdIndexes: array(string())
56912
+ });
56913
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
56914
+ var SecretRefsList = array(string());
56915
+ /**
56916
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
56917
+ * All ops of one call share ONE database file and ONE `transactFamily`.
56918
+ */
56919
+ var RecordTransactOpSchema = discriminatedUnion("op", [
56920
+ object({
56921
+ op: literal("put"),
56922
+ kind: RecordKindNameSchema,
56923
+ record: RecordInputSchema,
56924
+ ifRev: number().int().min(0).optional()
56925
+ }),
56926
+ object({
56927
+ op: literal("patch"),
56928
+ kind: RecordKindNameSchema,
56929
+ id: RecordIdSchema,
56930
+ data: RecordBodySchema,
56931
+ at: number().int().nullable().optional(),
56932
+ expiresAt: number().int().nullable().optional(),
56933
+ ifRev: number().int().min(1).optional()
56934
+ }),
56935
+ object({
56936
+ op: literal("delete"),
56937
+ kind: RecordKindNameSchema,
56938
+ id: RecordIdSchema
56939
+ })
56940
+ ]);
56941
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
56942
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
56943
+ kind: "mutation",
56944
+ auth: "admin"
56945
+ }), method(object({
56946
+ ...KindShape,
56947
+ id: RecordIdSchema
56948
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
56949
+ ...KindShape,
56950
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56951
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
56952
+ ...KindShape,
56953
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56954
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
56955
+ ...KindShape,
56956
+ record: RecordInputSchema,
56957
+ /** Compare-and-set. `0` means "the row must not exist". */
56958
+ ifRev: number().int().min(0).optional()
56959
+ }), object({
56960
+ rev: number().int(),
56961
+ releasedRefs: SecretRefsList
56962
+ }), {
56963
+ kind: "mutation",
56964
+ auth: "admin"
56965
+ }), method(object({
56966
+ ...KindShape,
56967
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
56968
+ }), object({
56969
+ count: number().int(),
56970
+ releasedRefs: SecretRefsList
56971
+ }), {
56972
+ kind: "mutation",
56973
+ auth: "admin"
56974
+ }), method(object({
56975
+ ...KindShape,
56976
+ id: RecordIdSchema,
56977
+ data: RecordBodySchema,
56978
+ at: number().int().nullable().optional(),
56979
+ expiresAt: number().int().nullable().optional(),
56980
+ ifRev: number().int().min(1).optional()
56981
+ }), object({
56982
+ rev: number().int(),
56983
+ releasedRefs: SecretRefsList
56984
+ }), {
56985
+ kind: "mutation",
56986
+ auth: "admin"
56987
+ }), method(object({
56988
+ ...KindShape,
56989
+ id: RecordIdSchema
56990
+ }), object({
56991
+ deleted: boolean(),
56992
+ secretRefs: SecretRefsList
56993
+ }), {
56994
+ kind: "mutation",
56995
+ auth: "admin"
56996
+ }), method(object({
56997
+ ...KindShape,
56998
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56999
+ }), object({
57000
+ deleted: number().int(),
57001
+ secretRefs: SecretRefsList
57002
+ }), {
57003
+ kind: "mutation",
57004
+ auth: "admin"
57005
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
57006
+ ...KindShape,
57007
+ ...TimeWindowShape,
57008
+ where: RecordWhereSchema.optional()
57009
+ }), object({ count: number().int() }), { auth: "admin" }), method(object({
57010
+ ...KindShape,
57011
+ deviceId: number().int().optional(),
57012
+ from: number(),
57013
+ to: number(),
57014
+ bucketMs: number().int().positive()
57015
+ }), object({ buckets: array(object({
57016
+ bucketStart: number(),
57017
+ count: number().int()
57018
+ })) }), { auth: "admin" }), method(object({
57019
+ ...KindShape,
57020
+ op: _enum([
57021
+ "sum",
57022
+ "min",
57023
+ "max",
57024
+ "count"
57025
+ ]),
57026
+ field: RecordFieldPathSchema.optional(),
57027
+ groupBy: RecordFieldPathSchema.optional(),
57028
+ where: RecordWhereSchema.optional()
57029
+ }), object({ rows: array(object({
57030
+ group: union([
57031
+ string(),
57032
+ number(),
57033
+ _null()
57034
+ ]),
57035
+ value: number().nullable()
57036
+ })) }), { auth: "admin" }), method(object({
57037
+ ...KindShape,
57038
+ deviceId: number().int().optional(),
57039
+ atBefore: number().optional(),
57040
+ where: RecordWhereSchema.optional()
57041
+ }), object({
57042
+ deleted: number().int(),
57043
+ secretRefs: SecretRefsList
57044
+ }), {
57045
+ kind: "mutation",
57046
+ auth: "admin"
57047
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
57048
+ results: array(object({
57049
+ rev: number().int().nullable(),
57050
+ deleted: boolean()
57051
+ })),
57052
+ releasedRefs: SecretRefsList,
57053
+ secretRefs: SecretRefsList
57054
+ }), {
57055
+ kind: "mutation",
57056
+ auth: "admin"
57057
+ }), method(object({
57058
+ deviceId: number().int(),
57059
+ kinds: array(RecordKindNameSchema).optional()
57060
+ }), object({
57061
+ deleted: record(string(), number().int()),
57062
+ secretRefs: SecretRefsList
57063
+ }), {
57064
+ kind: "mutation",
57065
+ auth: "admin"
57066
+ }), method(object(KindShape), object({ id: number().int() }), {
57067
+ kind: "mutation",
57068
+ auth: "admin"
57069
+ });
57070
+ /**
57071
+ * The wire shapes of the migration API, version 1 (planning#447).
57072
+ *
57073
+ * A migration is code that outlives the server it was written against: an addon at
57074
+ * version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
57075
+ * the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
57076
+ * is the contract of every method of it, one Zod pair per method.
57077
+ *
57078
+ * APPEND-ONLY. A released method is never removed and never changes meaning: an input
57079
+ * field is never removed or made required, an enum never narrows, an output field is
57080
+ * never removed. A change that cannot be expressed that way is `migration-api.v2`, next
57081
+ * to this file. `scripts/check-migration-api-append-only.ts` holds the line against
57082
+ * `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
57083
+ * `it('<id>'` in `migration-api.contract.spec.ts`.
57084
+ */
57085
+ var Rows = record(string(), unknown());
57086
+ var IntegrationId = object({ integrationId: string().min(1) });
57087
+ var RecordId = object({ id: string().min(1) });
57088
+ var RecordRowSchema = object({
57089
+ id: string(),
57090
+ deviceId: number().int().nullable(),
57091
+ at: number().nullable(),
57092
+ expiresAt: number().nullable(),
57093
+ rev: number().int(),
57094
+ updatedAt: number(),
57095
+ data: Rows
57096
+ });
57097
+ var RecordPutShape = {
57098
+ id: string().min(1),
57099
+ deviceId: number().int().optional(),
57100
+ at: number().int().optional(),
57101
+ expiresAt: number().int().optional(),
57102
+ data: Rows
57103
+ };
57104
+ var MigrationIntegrationSchema = object({
57105
+ id: string(),
57106
+ addonId: string(),
57107
+ name: string(),
57108
+ info: Rows,
57109
+ originStableId: string().optional()
57110
+ });
57111
+ var MigrationDeviceSchema = object({
57112
+ id: number().int(),
57113
+ integrationId: string().nullable().optional(),
57114
+ /** The owning device; null or absent for a root. */
57115
+ parentDeviceId: number().int().nullable().optional(),
57116
+ type: string().optional(),
57117
+ name: string().optional(),
57118
+ stableId: string().optional()
57119
+ });
57120
+ /** What the engine says about the migration API it serves: lets an addon notice an older server. */
57121
+ var MigrationApiInfoSchema = object({
57122
+ version: number().int(),
57123
+ methods: array(string())
57124
+ });
57125
+ object({
57126
+ message: string(),
57127
+ meta: Rows.optional(),
57128
+ /** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
57129
+ tags: object({ deviceId: number().int().optional() }).optional()
57130
+ });
57131
+ object({
57132
+ owner: string().min(1),
57133
+ metas: array(MigrationMetaSchema)
57134
+ }), object({
57135
+ bootId: string(),
57136
+ snapshot: _enum([
57137
+ "none",
57138
+ "written",
57139
+ "refused"
57140
+ ]),
57141
+ steps: array(MigrationOwnerStepSchema),
57142
+ /** The migration API the engine serves; absent from a server that predates it. */
57143
+ migrationApi: MigrationApiInfoSchema.optional()
57144
+ }), object({ id: string().min(1) }), object({
57145
+ proceed: boolean(),
57146
+ state: union([literal("pending"), MigrationStatusSchema]),
57147
+ reason: string().nullable()
57148
+ }), object({
57149
+ id: string().min(1),
57150
+ result: MigrationOwnerResultSchema
57151
+ }), object({
57152
+ status: MigrationStatusSchema,
57153
+ error: string().nullable(),
57154
+ rowsChanged: number().int().nonnegative()
57155
+ }), object({
57156
+ rows: number().int().nonnegative(),
57157
+ secrets: number().int().nonnegative()
57158
+ }), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
57159
+ present: boolean(),
57160
+ removed: Rows
57161
+ }), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
57162
+ ...RecordPutShape,
57163
+ ifRev: number().int().optional()
57164
+ }), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
57165
+ partial: Rows,
57166
+ ifRev: number().int().optional()
57167
+ }), object({ rev: number().int() }), object({ deleted: boolean() }), object({
57168
+ deviceId: number().int().optional(),
57169
+ atFrom: number().optional(),
57170
+ atTo: number().optional(),
57171
+ where: RecordWhereSchema.optional(),
57172
+ limit: number().int().min(1).optional(),
57173
+ cursor: string().min(1).optional(),
57174
+ resolveSecrets: boolean().optional()
57175
+ }), object({
57176
+ records: array(RecordRowSchema),
57177
+ nextCursor: string().nullable()
57178
+ }), object({
57179
+ deviceId: number().int().optional(),
57180
+ atBefore: number().optional(),
57181
+ where: RecordWhereSchema.optional()
57182
+ }), 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({
57183
+ ref: string().min(1),
57184
+ value: string()
57185
+ }), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
57186
+ addonId: string().min(1),
57187
+ integrationId: string().min(1).optional()
57188
+ }), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
57189
+ id: number().int(),
57190
+ integrationId: string().nullable(),
57191
+ /** Whether the device carries any stored config; its content is not offered. */
57192
+ hasConfig: boolean()
57193
+ }).nullable(), object({
57194
+ deviceId: number().int(),
57195
+ integrationId: string().min(1)
57196
+ }), object({}), object({}), object({ nodeId: string() }), object({}), object({});
56866
57197
  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({
56867
57198
  id: string().min(1),
56868
57199
  error: string().min(1)
@@ -56892,7 +57223,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56892
57223
  "written",
56893
57224
  "refused"
56894
57225
  ]),
56895
- steps: array(MigrationOwnerStepSchema)
57226
+ steps: array(MigrationOwnerStepSchema),
57227
+ /** The migration API this engine serves (additive: a server that predates it omits it). */
57228
+ migrationApi: MigrationApiInfoSchema.optional()
56896
57229
  }), {
56897
57230
  kind: "mutation",
56898
57231
  auth: "admin"
@@ -59559,7 +59892,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
59559
59892
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
59560
59893
  kind: "mutation",
59561
59894
  auth: "admin"
59562
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59895
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
59896
+ kind: "query",
59897
+ auth: "admin"
59898
+ }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59563
59899
  kind: "query",
59564
59900
  auth: "admin"
59565
59901
  }), method(object({}), array(RelocateJobSchema).readonly(), {
@@ -61966,7 +62302,7 @@ var BrokerStatusSchema$1 = _enum([
61966
62302
  "unreachable",
61967
62303
  "tls-error"
61968
62304
  ]);
61969
- var BrokerInfoSchema$1 = object({
62305
+ var BrokerInfoSchema = object({
61970
62306
  id: string(),
61971
62307
  name: string(),
61972
62308
  url: string(),
@@ -62029,7 +62365,7 @@ var StatusSchema = object({
62029
62365
  brokerCount: number(),
62030
62366
  embeddedRunning: boolean()
62031
62367
  });
62032
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
62368
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
62033
62369
  kind: "mutation",
62034
62370
  auth: "admin"
62035
62371
  }), method(IdInputSchema, _void(), {
@@ -68347,253 +68683,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
68347
68683
  auth: "admin"
68348
68684
  });
68349
68685
  /**
68350
- * records - an addon's own facts and documents (D881): ledgers, audit rows,
68351
- * config documents, mirrors. The third storage API next to `ctx.settings`
68352
- * (operator intent, D870) and device-history (numbers over time, D850).
68353
- *
68354
- * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
68355
- * crossed a process and stated by a hub in-process caller (`ctx.records` does
68356
- * that with the addon id closed over). It is deliberately NOT in any input
68357
- * schema, so a session can never supply it, and a call with no stamp is refused
68358
- * `records/no-caller`. A kind is readable and writable only by its owner; other
68359
- * addons go through the owner's capability.
68360
- *
68361
- * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
68362
- * never `[]`. Secret fields are never on the wire: a read shows the
68363
- * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
68364
- * resolves itself, and a write may carry only a keychain marker bound to the
68365
- * row's own ref.
68366
- *
68367
- * Internal: addons reach it through `ctx.records`, never directly.
68368
- */
68369
- var RecordIdSchema = string().min(1).max(256);
68370
- /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
68371
- var RecordBodySchema = record(string(), unknown());
68372
- var RecordInputSchema = object({
68373
- id: RecordIdSchema,
68374
- /** The device this row belongs to (D865 removal, per-device reads). */
68375
- deviceId: number().int().optional(),
68376
- /** The domain clock, ms: observed / created / opened. */
68377
- at: number().int().optional(),
68378
- /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
68379
- expiresAt: number().int().optional(),
68380
- data: RecordBodySchema
68381
- });
68382
- var RecordViewSchema = object({
68383
- id: string(),
68384
- deviceId: number().int().nullable(),
68385
- at: number().nullable(),
68386
- expiresAt: number().nullable(),
68387
- rev: number().int(),
68388
- updatedAt: number(),
68389
- data: RecordBodySchema
68390
- });
68391
- /** Declared index fields only. Anything else does not compile, by design. */
68392
- var RecordWhereSchema = record(RecordFieldPathSchema, union([
68393
- string(),
68394
- number(),
68395
- boolean(),
68396
- object({ in: array(union([
68397
- string(),
68398
- number(),
68399
- boolean()
68400
- ])).min(1).max(RECORD_BATCH_MAX) }),
68401
- object({
68402
- gt: union([string(), number()]).optional(),
68403
- gte: union([string(), number()]).optional(),
68404
- lt: union([string(), number()]).optional(),
68405
- lte: union([string(), number()]).optional()
68406
- }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
68407
- ]));
68408
- var KindShape = { kind: RecordKindNameSchema };
68409
- var TimeWindowShape = {
68410
- deviceId: number().int().optional(),
68411
- atFrom: number().optional(),
68412
- atTo: number().optional()
68413
- };
68414
- var RecordQueryInputSchema = object({
68415
- ...KindShape,
68416
- ...TimeWindowShape,
68417
- where: RecordWhereSchema.optional(),
68418
- /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
68419
- order: _enum(["at", "id"]).optional(),
68420
- desc: boolean().optional(),
68421
- limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
68422
- /** Opaque keyset cursor from a previous answer. There is no offset. */
68423
- cursor: string().min(1).optional()
68424
- });
68425
- var RecordQueryResultSchema = object({
68426
- records: array(RecordViewSchema),
68427
- nextCursor: string().nullable()
68428
- });
68429
- var RecordKindStateSchema = _enum([
68430
- "created",
68431
- "unchanged",
68432
- "updated"
68433
- ]);
68434
- var DeclareKindResultSchema = object({
68435
- kindId: number().int(),
68436
- state: RecordKindStateSchema,
68437
- createdIndexes: array(string())
68438
- });
68439
- /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
68440
- var SecretRefsList = array(string());
68441
- /**
68442
- * One op of a `transact`: the single-row writes, each on a kind of the caller.
68443
- * All ops of one call share ONE database file and ONE `transactFamily`.
68444
- */
68445
- var RecordTransactOpSchema = discriminatedUnion("op", [
68446
- object({
68447
- op: literal("put"),
68448
- kind: RecordKindNameSchema,
68449
- record: RecordInputSchema,
68450
- ifRev: number().int().min(0).optional()
68451
- }),
68452
- object({
68453
- op: literal("patch"),
68454
- kind: RecordKindNameSchema,
68455
- id: RecordIdSchema,
68456
- data: RecordBodySchema,
68457
- at: number().int().nullable().optional(),
68458
- expiresAt: number().int().nullable().optional(),
68459
- ifRev: number().int().min(1).optional()
68460
- }),
68461
- object({
68462
- op: literal("delete"),
68463
- kind: RecordKindNameSchema,
68464
- id: RecordIdSchema
68465
- })
68466
- ]);
68467
- var RecordSecretRefsSchema = record(string(), record(string(), string()));
68468
- method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
68469
- kind: "mutation",
68470
- auth: "admin"
68471
- }), method(object({
68472
- ...KindShape,
68473
- id: RecordIdSchema
68474
- }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
68475
- ...KindShape,
68476
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68477
- }), array(RecordViewSchema), { auth: "admin" }), method(object({
68478
- ...KindShape,
68479
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68480
- }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
68481
- ...KindShape,
68482
- record: RecordInputSchema,
68483
- /** Compare-and-set. `0` means "the row must not exist". */
68484
- ifRev: number().int().min(0).optional()
68485
- }), object({
68486
- rev: number().int(),
68487
- releasedRefs: SecretRefsList
68488
- }), {
68489
- kind: "mutation",
68490
- auth: "admin"
68491
- }), method(object({
68492
- ...KindShape,
68493
- records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
68494
- }), object({
68495
- count: number().int(),
68496
- releasedRefs: SecretRefsList
68497
- }), {
68498
- kind: "mutation",
68499
- auth: "admin"
68500
- }), method(object({
68501
- ...KindShape,
68502
- id: RecordIdSchema,
68503
- data: RecordBodySchema,
68504
- at: number().int().nullable().optional(),
68505
- expiresAt: number().int().nullable().optional(),
68506
- ifRev: number().int().min(1).optional()
68507
- }), object({
68508
- rev: number().int(),
68509
- releasedRefs: SecretRefsList
68510
- }), {
68511
- kind: "mutation",
68512
- auth: "admin"
68513
- }), method(object({
68514
- ...KindShape,
68515
- id: RecordIdSchema
68516
- }), object({
68517
- deleted: boolean(),
68518
- secretRefs: SecretRefsList
68519
- }), {
68520
- kind: "mutation",
68521
- auth: "admin"
68522
- }), method(object({
68523
- ...KindShape,
68524
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68525
- }), object({
68526
- deleted: number().int(),
68527
- secretRefs: SecretRefsList
68528
- }), {
68529
- kind: "mutation",
68530
- auth: "admin"
68531
- }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
68532
- ...KindShape,
68533
- ...TimeWindowShape,
68534
- where: RecordWhereSchema.optional()
68535
- }), object({ count: number().int() }), { auth: "admin" }), method(object({
68536
- ...KindShape,
68537
- deviceId: number().int().optional(),
68538
- from: number(),
68539
- to: number(),
68540
- bucketMs: number().int().positive()
68541
- }), object({ buckets: array(object({
68542
- bucketStart: number(),
68543
- count: number().int()
68544
- })) }), { auth: "admin" }), method(object({
68545
- ...KindShape,
68546
- op: _enum([
68547
- "sum",
68548
- "min",
68549
- "max",
68550
- "count"
68551
- ]),
68552
- field: RecordFieldPathSchema.optional(),
68553
- groupBy: RecordFieldPathSchema.optional(),
68554
- where: RecordWhereSchema.optional()
68555
- }), object({ rows: array(object({
68556
- group: union([
68557
- string(),
68558
- number(),
68559
- _null()
68560
- ]),
68561
- value: number().nullable()
68562
- })) }), { auth: "admin" }), method(object({
68563
- ...KindShape,
68564
- deviceId: number().int().optional(),
68565
- atBefore: number().optional(),
68566
- where: RecordWhereSchema.optional()
68567
- }), object({
68568
- deleted: number().int(),
68569
- secretRefs: SecretRefsList
68570
- }), {
68571
- kind: "mutation",
68572
- auth: "admin"
68573
- }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
68574
- results: array(object({
68575
- rev: number().int().nullable(),
68576
- deleted: boolean()
68577
- })),
68578
- releasedRefs: SecretRefsList,
68579
- secretRefs: SecretRefsList
68580
- }), {
68581
- kind: "mutation",
68582
- auth: "admin"
68583
- }), method(object({
68584
- deviceId: number().int(),
68585
- kinds: array(RecordKindNameSchema).optional()
68586
- }), object({
68587
- deleted: record(string(), number().int()),
68588
- secretRefs: SecretRefsList
68589
- }), {
68590
- kind: "mutation",
68591
- auth: "admin"
68592
- }), method(object(KindShape), object({ id: number().int() }), {
68593
- kind: "mutation",
68594
- auth: "admin"
68595
- });
68596
- /**
68597
68686
  * record-entries - the ONE admin surface for every list of user-created entries
68598
68687
  * (D881): notification targets, brokers, LLM profiles, anything an addon
68599
68688
  * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
@@ -77149,20 +77238,6 @@ Object.freeze({
77149
77238
  access: "create",
77150
77239
  role: "viewer"
77151
77240
  },
77152
- "broker.add": {
77153
- capName: "broker",
77154
- capScope: "system",
77155
- addonId: null,
77156
- access: "create",
77157
- role: "admin"
77158
- },
77159
- "broker.get": {
77160
- capName: "broker",
77161
- capScope: "system",
77162
- addonId: null,
77163
- access: "view",
77164
- role: "admin"
77165
- },
77166
77241
  "broker.getBrokerConfig": {
77167
77242
  capName: "broker",
77168
77243
  capScope: "system",
@@ -77170,20 +77245,6 @@ Object.freeze({
77170
77245
  access: "view",
77171
77246
  role: "admin"
77172
77247
  },
77173
- "broker.getSettings": {
77174
- capName: "broker",
77175
- capScope: "system",
77176
- addonId: null,
77177
- access: "view",
77178
- role: "admin"
77179
- },
77180
- "broker.getSettingsSchema": {
77181
- capName: "broker",
77182
- capScope: "system",
77183
- addonId: null,
77184
- access: "view",
77185
- role: "admin"
77186
- },
77187
77248
  "broker.getState": {
77188
77249
  capName: "broker",
77189
77250
  capScope: "system",
@@ -77198,20 +77259,6 @@ Object.freeze({
77198
77259
  access: "view",
77199
77260
  role: "admin"
77200
77261
  },
77201
- "broker.list": {
77202
- capName: "broker",
77203
- capScope: "system",
77204
- addonId: null,
77205
- access: "view",
77206
- role: "admin"
77207
- },
77208
- "broker.listProviders": {
77209
- capName: "broker",
77210
- capScope: "system",
77211
- addonId: null,
77212
- access: "view",
77213
- role: "admin"
77214
- },
77215
77262
  "broker.publish": {
77216
77263
  capName: "broker",
77217
77264
  capScope: "system",
@@ -77219,20 +77266,6 @@ Object.freeze({
77219
77266
  access: "create",
77220
77267
  role: "admin"
77221
77268
  },
77222
- "broker.remove": {
77223
- capName: "broker",
77224
- capScope: "system",
77225
- addonId: null,
77226
- access: "delete",
77227
- role: "admin"
77228
- },
77229
- "broker.setSettings": {
77230
- capName: "broker",
77231
- capScope: "system",
77232
- addonId: null,
77233
- access: "create",
77234
- role: "admin"
77235
- },
77236
77269
  "broker.subscribe": {
77237
77270
  capName: "broker",
77238
77271
  capScope: "system",
@@ -77240,20 +77273,6 @@ Object.freeze({
77240
77273
  access: "create",
77241
77274
  role: "admin"
77242
77275
  },
77243
- "broker.testConnection": {
77244
- capName: "broker",
77245
- capScope: "system",
77246
- addonId: null,
77247
- access: "create",
77248
- role: "admin"
77249
- },
77250
- "broker.testSettings": {
77251
- capName: "broker",
77252
- capScope: "system",
77253
- addonId: null,
77254
- access: "create",
77255
- role: "admin"
77256
- },
77257
77276
  "broker.unsubscribe": {
77258
77277
  capName: "broker",
77259
77278
  capScope: "system",
@@ -82217,6 +82236,13 @@ Object.freeze({
82217
82236
  access: "view",
82218
82237
  role: "viewer"
82219
82238
  },
82239
+ "pipelineAnalytics.getMediaLocationCensus": {
82240
+ capName: "pipeline-analytics",
82241
+ capScope: "device",
82242
+ addonId: null,
82243
+ access: "view",
82244
+ role: "admin"
82245
+ },
82220
82246
  "pipelineAnalytics.getMediaReclaimStatus": {
82221
82247
  capName: "pipeline-analytics",
82222
82248
  capScope: "device",
@@ -91158,6 +91184,23 @@ object({
91158
91184
  delay: number().nonnegative().optional(),
91159
91185
  commands: record(string(), unknown())
91160
91186
  });
91187
+ var BrokerInUseError = class extends Error {
91188
+ brokerId;
91189
+ users;
91190
+ name = "BrokerInUseError";
91191
+ constructor(brokerId, users) {
91192
+ super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
91193
+ this.brokerId = brokerId;
91194
+ this.users = users;
91195
+ }
91196
+ };
91197
+ /** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
91198
+ async function assertBrokerUnused(brokerId, finders) {
91199
+ const users = [];
91200
+ for (const finder of finders) users.push(...await finder.find(brokerId));
91201
+ if (users.length > 0) throw new BrokerInUseError(brokerId, users);
91202
+ }
91203
+ entryBody({ brokerId: string() });
91161
91204
  //#endregion
91162
91205
  //#region src/relay-record-kinds.ts
91163
91206
  /**
@@ -91407,29 +91450,6 @@ var MessageRing = class {
91407
91450
  }
91408
91451
  };
91409
91452
  //#endregion
91410
- //#region src/mailbox-record-kinds.ts
91411
- /**
91412
- * The `smtp-inbound` mailbox as a record (D999): one `mailboxes` row per mailbox, owned by
91413
- * `smtp-nodemailer`. The row id is the broker id. The SMTP AUTH password is a keychain-bound secret
91414
- * field; nothing else holds a copy.
91415
- */
91416
- var MailboxBodySchema = entryBody({
91417
- port: number().int().min(1).max(65535),
91418
- bindHost: string().min(1),
91419
- domain: string().min(1),
91420
- requireAuth: boolean(),
91421
- username: string(),
91422
- password: string().optional().meta({ secret: true }),
91423
- starttls: boolean(),
91424
- maxMessageBytes: number().int().positive()
91425
- });
91426
- var mailboxesKind = defineEntryKind("mailboxes", {
91427
- title: "Inbound mailboxes",
91428
- nameField: "name",
91429
- schema: MailboxBodySchema,
91430
- writeTrigger: "an operator (or a consumer addon, once) adds or edits an inbound mailbox"
91431
- });
91432
- //#endregion
91433
91453
  //#region src/inbound-broker.ts
91434
91454
  /**
91435
91455
  * The `smtp-inbound` broker kind (D999): a mailbox a device can send alarm mail to.
@@ -91443,7 +91463,6 @@ var mailboxesKind = defineEntryKind("mailboxes", {
91443
91463
  var REBIND_DELAY_MS = 5e3;
91444
91464
  var MAX_REBIND_ATTEMPTS = 6;
91445
91465
  var NO_INTERNAL_HOST_REASON = "The hub has no internal address. Flag one as \"Internal\" under System → Network; cameras need it to reach this mailbox.";
91446
- var KIND_LABEL = "Inbound mailbox (SMTP)";
91447
91466
  var PAGE = 500;
91448
91467
  /** The hostname of a role's resolved base URL (`http://192.0.2.10:4480` → `192.0.2.10`). */
91449
91468
  function hostFromRole(resolved) {
@@ -91516,6 +91535,20 @@ var SmtpInboundBroker = class {
91516
91535
  this.rebindTimers.clear();
91517
91536
  await Promise.all([...this.runtimes.keys()].map((id) => this.unbind(id)));
91518
91537
  }
91538
+ /** Bring the listener in line with the record after the instances page changed it. */
91539
+ reconcileMailbox(id) {
91540
+ return this.reconcile(id);
91541
+ }
91542
+ /** The runtime's view of one mailbox; null while it has no listener. */
91543
+ runtimeOf(id) {
91544
+ const runtime = this.runtimes.get(id);
91545
+ if (runtime === void 0) return null;
91546
+ return {
91547
+ status: runtime.status,
91548
+ error: runtime.error,
91549
+ checkedAt: runtime.checkedAt
91550
+ };
91551
+ }
91519
91552
  reconcile(id) {
91520
91553
  const next = (this.chains.get(id) ?? Promise.resolve()).then(() => this.applyRecord(id));
91521
91554
  this.chains.set(id, next.catch(() => void 0));
@@ -91633,28 +91666,6 @@ var SmtpInboundBroker = class {
91633
91666
  payload: message
91634
91667
  });
91635
91668
  }
91636
- async exists(id) {
91637
- return await this.deps.handle.get(id) !== null;
91638
- }
91639
- async info(id) {
91640
- const row = await this.deps.handle.get(id);
91641
- if (row === null) return null;
91642
- const runtime = this.runtimes.get(id);
91643
- return {
91644
- id,
91645
- addonId: this.deps.addonId,
91646
- name: row.data.name,
91647
- kind: SMTP_INBOUND_KIND,
91648
- status: runtime?.status ?? "disconnected",
91649
- info: {
91650
- port: row.data.port,
91651
- domain: row.data.domain,
91652
- recipients: runtime === void 0 ? 0 : [...runtime.subscriptions.values()].reduce((n, s) => n + s.recipients.size, 0)
91653
- },
91654
- lastCheckedAt: runtime?.checkedAt ?? null,
91655
- error: runtime?.error ?? null
91656
- };
91657
- }
91658
91669
  async subscribe(brokerId, filter) {
91659
91670
  const parsed = SmtpInboundFilterSchema.parse(filter);
91660
91671
  const runtime = this.runtimes.get(brokerId);
@@ -91674,85 +91685,19 @@ var SmtpInboundBroker = class {
91674
91685
  runtime.subscriptions = next;
91675
91686
  return { subscriptionId: id };
91676
91687
  }
91688
+ /** The subscribers (camera email push) that still read one mailbox. */
91689
+ subscriberUsers() {
91690
+ return { find: async (brokerId) => [...this.runtimes.get(brokerId)?.subscriptions.values() ?? []].map((sub) => ({
91691
+ kind: "subscriber",
91692
+ id: sub.subscriberId
91693
+ })) };
91694
+ }
91695
+ /**
91696
+ * The `broker` cap is the bus only (D1013): the mailboxes themselves are the `mailboxes` kind of
91697
+ * the addon's `integration-instances` provider (`mailbox-instances.ts`).
91698
+ */
91677
91699
  provider() {
91678
- const owns = (id) => this.exists(id);
91679
91700
  return {
91680
- list: async ({ kind }) => {
91681
- if (kind !== void 0 && kind !== "smtp-inbound") return [];
91682
- const page = await this.deps.handle.query({ limit: PAGE });
91683
- return (await Promise.all(page.records.map((r) => this.info(r.id)))).flatMap((i) => i === null ? [] : [i]);
91684
- },
91685
- get: async ({ id }) => this.info(id),
91686
- listProviders: async () => [{
91687
- addonId: this.deps.addonId,
91688
- kinds: [{
91689
- kind: SMTP_INBOUND_KIND,
91690
- label: KIND_LABEL
91691
- }]
91692
- }],
91693
- add: async ({ kind, name, id, settings }) => {
91694
- if (kind !== "smtp-inbound") throw new Error(`smtp-nodemailer: only kind '${SMTP_INBOUND_KIND}' is handled here (got '${kind}')`);
91695
- const parsed = SmtpInboundSettingsSchema.parse(settings);
91696
- const brokerId = id ?? `smtp-inbound-${randomUUID().slice(0, 8)}`;
91697
- if (await this.exists(brokerId)) throw new Error(`broker id '${brokerId}' is already in use`);
91698
- const body = MailboxBodySchema.parse({
91699
- name,
91700
- port: parsed.port,
91701
- bindHost: parsed.bindHost,
91702
- domain: parsed.domain,
91703
- requireAuth: parsed.requireAuth,
91704
- username: parsed.username.length > 0 ? parsed.username : `camstack-${randomBytes(4).toString("hex")}`,
91705
- password: parsed.password.length > 0 ? parsed.password : randomBytes(18).toString("base64url"),
91706
- starttls: parsed.starttls,
91707
- maxMessageBytes: parsed.maxMessageBytes
91708
- });
91709
- await this.deps.handle.put({
91710
- id: brokerId,
91711
- data: body
91712
- }, { ifRev: 0 });
91713
- await this.reconcile(brokerId);
91714
- return { id: brokerId };
91715
- },
91716
- remove: async ({ id }) => {
91717
- if (!await owns(id)) return;
91718
- await this.deps.handle.delete(id);
91719
- await this.reconcile(id);
91720
- },
91721
- testConnection: async ({ id }) => {
91722
- if (!await owns(id)) return {
91723
- ok: false,
91724
- error: "unknown broker"
91725
- };
91726
- const runtime = this.runtimes.get(id);
91727
- if (runtime?.status === "connected") return {
91728
- ok: true,
91729
- latencyMs: 0
91730
- };
91731
- return {
91732
- ok: false,
91733
- error: runtime?.error ?? "The mailbox is not listening"
91734
- };
91735
- },
91736
- getSettings: async ({ id }) => {
91737
- const row = await this.deps.handle.get(id);
91738
- if (row === null) return null;
91739
- const { name: _name, password: _password, ...settings } = row.data;
91740
- return settings;
91741
- },
91742
- setSettings: async ({ id, settings }) => {
91743
- const row = await this.deps.handle.getResolved(id);
91744
- if (row === null) return;
91745
- const parsed = SmtpInboundSettingsSchema.partial().parse(settings);
91746
- const next = MailboxBodySchema.parse({
91747
- ...row.data,
91748
- ...Object.fromEntries(Object.entries(parsed).filter(([, v]) => v !== void 0 && v !== ""))
91749
- });
91750
- await this.deps.handle.put({
91751
- id,
91752
- data: next
91753
- });
91754
- await this.reconcile(id);
91755
- },
91756
91701
  getBrokerConfig: async ({ id }) => {
91757
91702
  let row;
91758
91703
  try {
@@ -91774,19 +91719,6 @@ var SmtpInboundBroker = class {
91774
91719
  starttls: row.data.starttls
91775
91720
  };
91776
91721
  },
91777
- getSettingsSchema: async ({ kind }) => kind === "smtp-inbound" ? mailboxSettingsSchema() : null,
91778
- testSettings: async ({ kind, settings }) => {
91779
- if (kind !== "smtp-inbound") return {
91780
- ok: false,
91781
- error: `unsupported kind: ${kind}`
91782
- };
91783
- const parsed = SmtpInboundSettingsSchema.safeParse(settings);
91784
- if (!parsed.success) return {
91785
- ok: false,
91786
- error: parsed.error.message
91787
- };
91788
- return probeBind(parsed.data.port, parsed.data.bindHost);
91789
- },
91790
91722
  publish: async () => {
91791
91723
  throw new Error("smtp-inbound: a mailbox only receives — publish is not supported");
91792
91724
  },
@@ -91814,23 +91746,6 @@ var SmtpInboundBroker = class {
91814
91746
  };
91815
91747
  }
91816
91748
  };
91817
- /** A bind probe: can this port be taken on this host right now? */
91818
- function probeBind(port, host) {
91819
- const started = Date.now();
91820
- return new Promise((resolve) => {
91821
- const probe = createServer();
91822
- probe.once("error", (err) => resolve({
91823
- ok: false,
91824
- error: err.message
91825
- }));
91826
- probe.listen(port, host, () => {
91827
- probe.close(() => resolve({
91828
- ok: true,
91829
- latencyMs: Date.now() - started
91830
- }));
91831
- });
91832
- });
91833
- }
91834
91749
  function mailboxSettingsSchema() {
91835
91750
  return { sections: [{
91836
91751
  id: "mailbox",
@@ -136120,6 +136035,145 @@ var SmtpServerListener = class {
136120
136035
  }
136121
136036
  };
136122
136037
  //#endregion
136038
+ //#region src/bind-probe.ts
136039
+ /** A bind probe: can this port be taken on this host right now? */
136040
+ function probeBind(port, host) {
136041
+ const started = Date.now();
136042
+ return new Promise((resolve) => {
136043
+ const probe = createServer();
136044
+ probe.once("error", (err) => resolve({
136045
+ ok: false,
136046
+ error: err.message
136047
+ }));
136048
+ probe.listen(port, host, () => {
136049
+ probe.close(() => resolve({
136050
+ ok: true,
136051
+ latencyMs: Date.now() - started
136052
+ }));
136053
+ });
136054
+ });
136055
+ }
136056
+ //#endregion
136057
+ //#region src/mailbox-record-kinds.ts
136058
+ /**
136059
+ * The `smtp-inbound` mailbox as a record (D999): one `mailboxes` row per mailbox, owned by
136060
+ * `smtp-nodemailer`. The row id is the broker id. The SMTP AUTH password is a keychain-bound secret
136061
+ * field; nothing else holds a copy.
136062
+ */
136063
+ var MailboxBodySchema = entryBody({
136064
+ port: number().int().min(1).max(65535),
136065
+ bindHost: string().min(1),
136066
+ domain: string().min(1),
136067
+ requireAuth: boolean(),
136068
+ username: string(),
136069
+ password: string().optional().meta({ secret: true }),
136070
+ starttls: boolean(),
136071
+ maxMessageBytes: number().int().positive()
136072
+ });
136073
+ var mailboxesKind = defineEntryKind("mailboxes", {
136074
+ title: "Inbound mailboxes",
136075
+ nameField: "name",
136076
+ schema: MailboxBodySchema,
136077
+ writeTrigger: "an operator (or a consumer addon, once) adds or edits an inbound mailbox"
136078
+ });
136079
+ //#endregion
136080
+ //#region src/mailbox-instances.ts
136081
+ /**
136082
+ * The inbound mailboxes as `integration-instances` (D1013): one instance per `mailboxes` record,
136083
+ * over the SAME records and ids the `smtp-inbound` broker serves; the `mailboxes` kind of the
136084
+ * composed provider (the relays are the `relays` kind). A change re-applies the listener; removing
136085
+ * a mailbox a subscriber (camera email push) still reads is refused.
136086
+ */
136087
+ var MAILBOX_INSTANCE_KIND = "mailboxes";
136088
+ function statusOf(runtime) {
136089
+ if (runtime === null) return {
136090
+ state: "unknown",
136091
+ label: "Not listening"
136092
+ };
136093
+ if (runtime.status === "connected") return {
136094
+ state: "ok",
136095
+ label: "Listening"
136096
+ };
136097
+ return {
136098
+ state: "error",
136099
+ label: runtime.status,
136100
+ ...runtime.error !== null ? { detail: runtime.error } : {}
136101
+ };
136102
+ }
136103
+ function newMailboxBody(name) {
136104
+ const defaults = SmtpInboundSettingsSchema.parse({});
136105
+ return MailboxBodySchema.parse({
136106
+ name,
136107
+ port: defaults.port,
136108
+ bindHost: defaults.bindHost,
136109
+ domain: defaults.domain,
136110
+ requireAuth: defaults.requireAuth,
136111
+ username: `camstack-${randomBytes(4).toString("hex")}`,
136112
+ password: randomBytes(18).toString("base64url"),
136113
+ starttls: defaults.starttls,
136114
+ maxMessageBytes: defaults.maxMessageBytes
136115
+ });
136116
+ }
136117
+ function buildMailboxInstances(records, deps) {
136118
+ const users = deps.inbound.subscriberUsers();
136119
+ const base = new RecordIntegrationInstances(records, mailboxesKind, {
136120
+ description: {
136121
+ role: "services",
136122
+ instanceMode: "multiple",
136123
+ kind: "mailbox",
136124
+ canTest: true
136125
+ },
136126
+ formSchema: () => mailboxSettingsSchema(),
136127
+ initialBody: (name) => newMailboxBody(name),
136128
+ mintId: () => `smtp-inbound-${randomUUID().slice(0, 8)}`,
136129
+ configured: () => true,
136130
+ health: (_body, id) => {
136131
+ const runtime = deps.inbound.runtimeOf(id);
136132
+ return {
136133
+ status: statusOf(runtime),
136134
+ ...runtime !== null ? { lastCheckedAt: runtime.checkedAt } : {}
136135
+ };
136136
+ },
136137
+ onChanged: (id) => deps.inbound.reconcileMailbox(id),
136138
+ test: async ({ id, values }) => {
136139
+ if (id === null) {
136140
+ const parsed = SmtpInboundSettingsSchema.safeParse(values);
136141
+ if (!parsed.success) return {
136142
+ success: false,
136143
+ status: "rejected",
136144
+ error: parsed.error.message
136145
+ };
136146
+ const bound = await probeBind(parsed.data.port, parsed.data.bindHost);
136147
+ return bound.ok ? {
136148
+ success: true,
136149
+ status: "validated",
136150
+ latencyMs: bound.latencyMs
136151
+ } : {
136152
+ success: false,
136153
+ status: "rejected",
136154
+ error: bound.error
136155
+ };
136156
+ }
136157
+ const runtime = deps.inbound.runtimeOf(id);
136158
+ return runtime?.status === "connected" ? {
136159
+ success: true,
136160
+ status: "validated"
136161
+ } : {
136162
+ success: false,
136163
+ status: "rejected",
136164
+ error: runtime?.error ?? "The mailbox is not listening"
136165
+ };
136166
+ }
136167
+ }).toProvider();
136168
+ return {
136169
+ ...base,
136170
+ deleteInstance: async (input) => {
136171
+ await assertBrokerUnused(input.id, [users]);
136172
+ return base.deleteInstance(input);
136173
+ }
136174
+ };
136175
+ }
136176
+ //#endregion
136123
136177
  //#region ../../node_modules/nodemailer/lib/mime-funcs/mime-types.js
136124
136178
  var require_mime_types = /* @__PURE__ */ __commonJSMin(((exports, module) => {
136125
136179
  var path$1 = __require("path");
@@ -145853,7 +145907,29 @@ var SmtpNodemailerAddon = class extends BaseAddon {
145853
145907
  },
145854
145908
  {
145855
145909
  capability: integrationInstancesCapability,
145856
- provider: this.instances.toProvider()
145910
+ provider: composeInstanceKinds([{
145911
+ kind: {
145912
+ id: "relays",
145913
+ label: "SMTP relay",
145914
+ functionCap: "smtp-provider",
145915
+ role: "services"
145916
+ },
145917
+ provider: this.instances.toProvider()
145918
+ }, {
145919
+ kind: {
145920
+ id: MAILBOX_INSTANCE_KIND,
145921
+ label: "Inbound mailbox (SMTP)",
145922
+ functionCap: "broker",
145923
+ brokerKind: SMTP_INBOUND_KIND,
145924
+ role: "services"
145925
+ },
145926
+ provider: buildMailboxInstances(records, { inbound })
145927
+ }], {
145928
+ role: "services",
145929
+ instanceMode: "multiple",
145930
+ kind: "relays",
145931
+ canTest: true
145932
+ })
145857
145933
  },
145858
145934
  {
145859
145935
  capability: brokerCapability,