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