@camstack/addon-provider-homeassistant 1.2.635 → 1.2.637

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.
@@ -5201,6 +5201,14 @@ function object(shape, params) {
5201
5201
  ...normalizeParams(params)
5202
5202
  });
5203
5203
  }
5204
+ function strictObject(shape, params) {
5205
+ return new ZodObject({
5206
+ type: "object",
5207
+ shape,
5208
+ catchall: never(),
5209
+ ...normalizeParams(params)
5210
+ });
5211
+ }
5204
5212
  function looseObject(shape, params) {
5205
5213
  return new ZodObject({
5206
5214
  type: "object",
@@ -5580,7 +5588,7 @@ var ZodIssueCode = {
5580
5588
  var ZodFirstPartyTypeKind;
5581
5589
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5582
5590
  //#endregion
5583
- //#region ../types/dist/sleep-BAvUCi_2.mjs
5591
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5584
5592
  /**
5585
5593
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5586
5594
  * window to float samples (D455).
@@ -8726,65 +8734,6 @@ var MODEL_FORMATS = [
8726
8734
  "pt",
8727
8735
  "gguf"
8728
8736
  ];
8729
- /**
8730
- * Per-call node pinning for `ctx.api` capability calls.
8731
- *
8732
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8733
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8734
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8735
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8736
- * call to that node.
8737
- *
8738
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8739
- * method args), so capability method signatures stay `nodeId`-free — node
8740
- * targeting is a property of the CALL, not of the method. The transport lifts
8741
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8742
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8743
- * which classifies a pinned agent node as `agent-child-forward`
8744
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8745
- *
8746
- * Usage at a call site:
8747
- *
8748
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8749
- */
8750
- /** tRPC `op.context` key carrying a per-call node pin. */
8751
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8752
- /**
8753
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8754
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8755
- *
8756
- * ## The id is normalised here, and it has to be
8757
- *
8758
- * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8759
- * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8760
- * compares a pin against real node ids, so such a pin matches nothing and the
8761
- * call fails with `no provider registered for cap "…"`. The local-first
8762
- * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8763
- * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8764
- * the thing you reach for when you specifically need THIS node — silently
8765
- * addressed a node that does not exist.
8766
- *
8767
- * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8768
- * own node to read the host's hardware-decode backend. It never once answered,
8769
- * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8770
- * while D67's whole premise was that the decoder addon is the authority on
8771
- * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8772
- * hardware", which was false.
8773
- *
8774
- * Normalising in the ONE constructor fixes every caller at once, which is why
8775
- * it is here and not at the call sites.
8776
- */
8777
- function nodePin(nodeId) {
8778
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8779
- }
8780
- /**
8781
- * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8782
- * is idempotent, so passing an already-clean id costs nothing.
8783
- */
8784
- function toNodeId(idOrRunnerId) {
8785
- const head = idOrRunnerId.split("/")[0];
8786
- return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8787
- }
8788
8737
  object({
8789
8738
  field: string().min(1),
8790
8739
  on: literal("advance")
@@ -8877,9 +8826,135 @@ function optionalMethod(input, output, options) {
8877
8826
  function event(data) {
8878
8827
  return { data };
8879
8828
  }
8829
+ /**
8830
+ * Per-call node pinning for `ctx.api` capability calls.
8831
+ *
8832
+ * A capability call normally resolves to its DEFAULT provider — a `singleton`
8833
+ * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8834
+ * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8835
+ * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8836
+ * call to that node.
8837
+ *
8838
+ * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8839
+ * method args), so capability method signatures stay `nodeId`-free — node
8840
+ * targeting is a property of the CALL, not of the method. The transport lifts
8841
+ * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8842
+ * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8843
+ * which classifies a pinned agent node as `agent-child-forward`
8844
+ * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8845
+ *
8846
+ * Usage at a call site:
8847
+ *
8848
+ * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8849
+ */
8850
+ /** tRPC `op.context` key carrying a per-call node pin. */
8851
+ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8852
+ /**
8853
+ * Build the tRPC request options that pin a single capability call to `nodeId`.
8854
+ * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8855
+ *
8856
+ * ## The id is normalised here, and it has to be
8857
+ *
8858
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8859
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8860
+ * compares a pin against real node ids, so such a pin matches nothing and the
8861
+ * call fails with `no provider registered for cap "…"`. The local-first
8862
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8863
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8864
+ * the thing you reach for when you specifically need THIS node — silently
8865
+ * addressed a node that does not exist.
8866
+ *
8867
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8868
+ * own node to read the host's hardware-decode backend. It never once answered,
8869
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8870
+ * while D67's whole premise was that the decoder addon is the authority on
8871
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8872
+ * hardware", which was false.
8873
+ *
8874
+ * Normalising in the ONE constructor fixes every caller at once, which is why
8875
+ * it is here and not at the call sites.
8876
+ */
8877
+ function nodePin(nodeId) {
8878
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8879
+ }
8880
+ /**
8881
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8882
+ * is idempotent, so passing an already-clean id costs nothing.
8883
+ */
8884
+ function toNodeId(idOrRunnerId) {
8885
+ const head = idOrRunnerId.split("/")[0];
8886
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8887
+ }
8888
+ /**
8889
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8890
+ * declarations. Every addon that contributes a UI page registers a
8891
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8892
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8893
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8894
+ * that admin-ui consumes.
8895
+ *
8896
+ * The split exists because the public listing has a different output
8897
+ * shape than the per-provider raw declarations, and we want both ends
8898
+ * to flow through codegen instead of relying on a hand-written wrapper.
8899
+ */
8900
+ var AddonPageDeclarationSchema = object({
8901
+ id: string(),
8902
+ label: string(),
8903
+ icon: string(),
8904
+ path: string(),
8905
+ /**
8906
+ * Module Federation remote name — must match the `name` field on the
8907
+ * page addon's `federation()` plugin config. Used by admin-ui's
8908
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8909
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8910
+ * contain hyphens).
8911
+ */
8912
+ remoteName: string(),
8913
+ /**
8914
+ * Bundle filename inside the addon's `dist/` dir served at
8915
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8916
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8917
+ * the static-file route can compute an mtime-based cache-buster URL
8918
+ * without a separate filesystem stat.
8919
+ */
8920
+ bundle: string(),
8921
+ /**
8922
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8923
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8924
+ * Any OTHER string creates (or joins) a custom section rendered after
8925
+ * the built-in groups; its label comes from `sectionLabel` (first
8926
+ * declaration wins), falling back to the id. Absent → the legacy
8927
+ * "Addon Pages" group.
8928
+ */
8929
+ section: string().optional(),
8930
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8931
+ sectionLabel: string().optional()
8932
+ });
8933
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8880
8934
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8881
8935
  var VersionOutputSchema$1 = object({ version: string() });
8882
8936
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8937
+ /**
8938
+ * The pipeline step-input shape and the run-result bridge, in a module that
8939
+ * imports nothing but zod and types.
8940
+ *
8941
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8942
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8943
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8944
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8945
+ * them, so every existing import keeps working; addon server code takes them
8946
+ * from `@camstack/types/addon`.
8947
+ */
8948
+ var PipelineStepInputSchema = lazy(() => object({
8949
+ addonId: string(),
8950
+ modelId: string().optional(),
8951
+ enabled: boolean().default(true),
8952
+ children: array(PipelineStepInputSchema).optional(),
8953
+ settings: record(string(), unknown()).optional(),
8954
+ jumpDeviceKey: string().optional()
8955
+ }));
8956
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8957
+ var PipelineRunResultBridge = custom();
8883
8958
  var StaticDirOutputSchema = object({ staticDir: string() });
8884
8959
  var VersionOutputSchema = object({ version: string() });
8885
8960
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9064,6 +9139,7 @@ var CAP_NAMES = Object.freeze([
9064
9139
  "network-link",
9065
9140
  "network-quality",
9066
9141
  "node-certificates",
9142
+ "node-removal-hook",
9067
9143
  "nodes",
9068
9144
  "notification-output",
9069
9145
  "notification-rules",
@@ -9074,6 +9150,7 @@ var CAP_NAMES = Object.freeze([
9074
9150
  "osd",
9075
9151
  "osd-manager",
9076
9152
  "pet-feeder",
9153
+ "pet-fountain",
9077
9154
  "pipeline-analytics",
9078
9155
  "pipeline-executor",
9079
9156
  "pipeline-orchestrator",
@@ -9094,6 +9171,7 @@ var CAP_NAMES = Object.freeze([
9094
9171
  "recording-onboard",
9095
9172
  "recording-signal",
9096
9173
  "records",
9174
+ "remote",
9097
9175
  "running-state",
9098
9176
  "scene-monitor",
9099
9177
  "script-runner",
@@ -9117,6 +9195,7 @@ var CAP_NAMES = Object.freeze([
9117
9195
  "system",
9118
9196
  "tamper",
9119
9197
  "temperature-sensor",
9198
+ "template",
9120
9199
  "terminal-session",
9121
9200
  "toast",
9122
9201
  "training",
@@ -10057,6 +10136,11 @@ Object.freeze({
10057
10136
  "standby"
10058
10137
  ]
10059
10138
  },
10139
+ {
10140
+ "path": "stateSource",
10141
+ "valueType": "enum",
10142
+ "enumValues": ["reported", "assumed"]
10143
+ },
10060
10144
  {
10061
10145
  "path": "volumeLevel",
10062
10146
  "valueType": "number"
@@ -10332,6 +10416,68 @@ Object.freeze({
10332
10416
  {
10333
10417
  "path": "volume",
10334
10418
  "valueType": "number"
10419
+ },
10420
+ {
10421
+ "path": "lastFeedGrams",
10422
+ "valueType": "number"
10423
+ },
10424
+ {
10425
+ "path": "lastFeedHopper1",
10426
+ "valueType": "number"
10427
+ },
10428
+ {
10429
+ "path": "lastFeedHopper2",
10430
+ "valueType": "number"
10431
+ }
10432
+ ],
10433
+ "pet-fountain": [
10434
+ {
10435
+ "path": "waterLevel",
10436
+ "valueType": "number"
10437
+ },
10438
+ {
10439
+ "path": "waterVolume",
10440
+ "valueType": "number"
10441
+ },
10442
+ {
10443
+ "path": "empty",
10444
+ "valueType": "boolean"
10445
+ },
10446
+ {
10447
+ "path": "pumpRunning",
10448
+ "valueType": "boolean"
10449
+ },
10450
+ {
10451
+ "path": "pumpIntervalEnabled",
10452
+ "valueType": "boolean"
10453
+ },
10454
+ {
10455
+ "path": "pumpOnTime",
10456
+ "valueType": "number"
10457
+ },
10458
+ {
10459
+ "path": "pumpOffTime",
10460
+ "valueType": "number"
10461
+ },
10462
+ {
10463
+ "path": "lastDrink",
10464
+ "valueType": "number"
10465
+ },
10466
+ {
10467
+ "path": "currentHourDrink",
10468
+ "valueType": "number"
10469
+ },
10470
+ {
10471
+ "path": "last24hDrink",
10472
+ "valueType": "number"
10473
+ },
10474
+ {
10475
+ "path": "todayDrink",
10476
+ "valueType": "number"
10477
+ },
10478
+ {
10479
+ "path": "error",
10480
+ "valueType": "string"
10335
10481
  }
10336
10482
  ],
10337
10483
  "power-meter": [
@@ -10871,7 +11017,7 @@ object({
10871
11017
  params: record(string(), union([string(), number()])).optional()
10872
11018
  });
10873
11019
  //#endregion
10874
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11020
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10875
11021
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10876
11022
  DeviceType["Camera"] = "camera";
10877
11023
  DeviceType["Hub"] = "hub";
@@ -10963,6 +11109,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10963
11109
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10964
11110
  * integrations sharing the same food/desiccant/hopper surface. */
10965
11111
  DeviceType["PetFeeder"] = "pet-feeder";
11112
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11113
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11114
+ * of a container and is BUILT as a composed device (D967). */
11115
+ DeviceType["PetFountain"] = "pet-fountain";
10966
11116
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10967
11117
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10968
11118
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11233,7 +11383,7 @@ function errMsg(err) {
11233
11383
  return String(err);
11234
11384
  }
11235
11385
  //#endregion
11236
- //#region ../types/dist/composition-DpFv_J6D.mjs
11386
+ //#region ../types/dist/composition-kSuOifuG.mjs
11237
11387
  /**
11238
11388
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11239
11389
  *
@@ -12318,12 +12468,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12318
12468
  ])
12319
12469
  })
12320
12470
  ]);
12321
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12471
+ var forwardArgUnion = union([
12472
+ string(),
12473
+ number(),
12474
+ boolean(),
12475
+ strictObject({ ["$input"]: string().min(1) }),
12476
+ record(string().min(1), lazy(() => forwardArgUnion))
12477
+ ]);
12478
+ var CompositionForwardArgSchema = forwardArgUnion;
12479
+ /**
12480
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12481
+ * call of `cap.method` on `source`. Without `args` the built method's input
12482
+ * passes through minus `deviceId`; with `args` the template IS the source's
12483
+ * whole input. `deviceId` is always the source's own, never an argument.
12484
+ */
12485
+ var CompositionForwardCommandSchema = object({
12322
12486
  kind: literal("forward"),
12323
12487
  source: CompositionSourceRefSchema,
12324
12488
  cap: string().min(1),
12325
- method: string().min(1)
12326
- }), CompositionCodeSourceSchema]);
12489
+ method: string().min(1),
12490
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12491
+ });
12492
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12327
12493
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12328
12494
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12329
12495
  /**
@@ -12333,7 +12499,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12333
12499
  */
12334
12500
  var CompositionItemEntrySchema = object({
12335
12501
  label: string().min(1).max(80),
12336
- fields: record(string().min(1), CompositionFieldSourceSchema)
12502
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12503
+ /**
12504
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12505
+ * is resettable and a `reset` call naming its key forwards here. The item key
12506
+ * selects the command, so it is never an argument: there is no pass-through.
12507
+ */
12508
+ reset: CompositionForwardCommandSchema.optional()
12337
12509
  });
12338
12510
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12339
12511
  kind: literal("fields"),
@@ -12345,7 +12517,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12345
12517
  * owns the WHOLE array field: the composed value holds exactly these items.
12346
12518
  */
12347
12519
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12348
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12520
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12349
12521
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12350
12522
  }), object({
12351
12523
  kind: literal("passthrough"),
@@ -16429,119 +16601,654 @@ object({
16429
16601
  lastError: string().nullable()
16430
16602
  });
16431
16603
  /**
16432
- * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16433
- * surface that admin-ui consumes through `useAddonPagesListPages()`.
16604
+ * notification-output — canonical, capability-gated notification delivery.
16434
16605
  *
16435
- * The provider iterates every `addon-pages-source` (collection) provider
16436
- * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
16437
- * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
16438
- * filesystem `mtime` cache-buster lets the browser pick up addon
16439
- * rebuilds without manual reload.
16606
+ * Apprise-derived model (see
16607
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16608
+ * callers emit ONE canonical `Notification`; each provider declares a
16609
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16610
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16611
+ * message to what the kind supports — callers never special-case a service.
16440
16612
  *
16441
- * The hub-local builtin `addon-pages-aggregator` (see
16442
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
16443
- * provider. Splitting the public aggregator from the raw collection
16444
- * keeps both ends in codegen — there's no hand-written
16445
- * `addon-pages.router.ts` wrapper anymore.
16613
+ * DESIGN DECISIONS (locked):
16614
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16615
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16616
+ * cap. Rationale: the admin UI needs one uniform surface across the
16617
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16618
+ * alternative would fork the UI per addon and cannot host the
16619
+ * discovery→adopt flow.
16620
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16621
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16622
+ * registered provider (notifiers addon + HA addon) so one catalog is
16623
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16624
+ * `addonId` the generated collection router extracts from the call input.
16625
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16626
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16627
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16628
+ * base64 fallback needed.
16629
+ *
16630
+ * TODO (deferred, closed-set change — separate decision): add
16631
+ * `providerKind: 'notify'` so notification providers surface on the unified
16632
+ * admin "Integrations" page.
16446
16633
  */
16447
- var AddonPageDeclarationSchema$1 = object({
16448
- id: string(),
16449
- label: string(),
16450
- icon: string(),
16451
- path: string(),
16452
- remoteName: string(),
16453
- bundle: string(),
16454
- section: string().optional(),
16455
- sectionLabel: string().optional()
16456
- });
16457
- var AddonPageInfoSchema = object({
16458
- addonId: string(),
16459
- page: AddonPageDeclarationSchema$1,
16460
- bundleUrl: string()
16461
- });
16462
- method(_void(), array(AddonPageInfoSchema).readonly());
16463
16634
  /**
16464
- * `addon-pages-source` — collection cap exposing per-provider raw page
16465
- * declarations. Every addon that contributes a UI page registers a
16466
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16467
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16468
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16469
- * that admin-ui consumes.
16635
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16636
+ * adapter picks what it supports and the degrade engine filters the rest.
16637
+ */
16638
+ var AttachmentMediaTypeSchema = _enum([
16639
+ "image",
16640
+ "video",
16641
+ "gif",
16642
+ "audio",
16643
+ "icon"
16644
+ ]);
16645
+ /**
16646
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16647
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16648
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16649
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16650
+ */
16651
+ var AttachmentSchema = object({
16652
+ mediaType: AttachmentMediaTypeSchema,
16653
+ url: string().optional(),
16654
+ bytes: _instanceof(Uint8Array).optional(),
16655
+ mime: string().optional(),
16656
+ name: string().optional()
16657
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16658
+ var NotificationFormatSchema = _enum([
16659
+ "text",
16660
+ "markdown",
16661
+ "html"
16662
+ ]);
16663
+ /**
16664
+ * The CLOSED icon vocabulary an action button may use.
16470
16665
  *
16471
- * The split exists because the public listing has a different output
16472
- * shape than the per-provider raw declarations, and we want both ends
16473
- * to flow through codegen instead of relying on a hand-written wrapper.
16666
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16667
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16668
+ * notices — the same class of gap as a zone vocabulary nothing produced
16669
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16670
+ * declares `actionIcons: false` and the degrade engine strips the field.
16671
+ *
16672
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16673
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16674
+ * differently.
16675
+ *
16676
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16677
+ *
16678
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16679
+ * the whole set onto its own renderer's vocabulary through a
16680
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16681
+ * fallback, so adding a member here fails every adapter's build until someone
16682
+ * decides its glyph, which is the only place that decision can be made
16683
+ * honestly.
16684
+ *
16685
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16686
+ * `disarm` straight through; iOS feeds that string to
16687
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16688
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16689
+ * mapping, and "the field is documented" is not "the value renders".
16690
+ *
16691
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16692
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16693
+ * so an addon that emits a token the running hub does not know does not lose an
16694
+ * icon — its whole `send` fails Zod validation and the notification never
16695
+ * arrives. Never emit a new token from an addon before the train carrying it.
16474
16696
  */
16475
- var AddonPageDeclarationSchema = object({
16697
+ var NotificationActionIconSchema = _enum([
16698
+ "acknowledge",
16699
+ "dismiss",
16700
+ "silence",
16701
+ "snooze",
16702
+ "view",
16703
+ "play",
16704
+ "open",
16705
+ "close",
16706
+ "lock",
16707
+ "unlock",
16708
+ "arm",
16709
+ "arm-home",
16710
+ "arm-away",
16711
+ "arm-night",
16712
+ "disarm",
16713
+ "light",
16714
+ "alert",
16715
+ "camera"
16716
+ ]);
16717
+ /** A single tap-through action button. */
16718
+ var NotificationActionSchema = object({
16476
16719
  id: string(),
16477
16720
  label: string(),
16478
- icon: string(),
16479
- path: string(),
16480
- /**
16481
- * Module Federation remote name — must match the `name` field on the
16482
- * page addon's `federation()` plugin config. Used by admin-ui's
16483
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16484
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16485
- * contain hyphens).
16486
- */
16487
- remoteName: string(),
16721
+ url: string().optional(),
16722
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16723
+ icon: NotificationActionIconSchema.optional(),
16488
16724
  /**
16489
- * Bundle filename inside the addon's `dist/` dir served at
16490
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16491
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16492
- * the static-file route can compute an mtime-based cache-buster URL
16493
- * without a separate filesystem stat.
16725
+ * Renders in a warning style where the notifier supports it.
16726
+ *
16727
+ * A HINT, never a gate. The callback's authority is its token and nothing
16728
+ * else — see `notification-center/action-token.ts` for what that does and
16729
+ * does not buy.
16494
16730
  */
16495
- bundle: string(),
16731
+ destructive: boolean().optional(),
16496
16732
  /**
16497
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16498
- * `'cluster'`, `'administration'` — the page renders inside that group.
16499
- * Any OTHER string creates (or joins) a custom section rendered after
16500
- * the built-in groups; its label comes from `sectionLabel` (first
16501
- * declaration wins), falling back to the id. Absent → the legacy
16502
- * "Addon Pages" group.
16733
+ * How the tap should REACH the url.
16734
+ *
16735
+ * `navigate` (absent, and every button authored before this field) opens it:
16736
+ * the phone leaves the notification and shows whatever the callback returns.
16737
+ * That is right for a button whose answer the operator wants to read.
16738
+ *
16739
+ * `background` fires it as a POST and stays put. It exists for the buttons
16740
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16741
+ * an answer to the notification, and being thrown into a browser tab to
16742
+ * confirm it costs more attention than the notification did. A backend that
16743
+ * cannot do a background call renders it as an ordinary link (the adapters
16744
+ * fall back rather than dropping the button), so this is a preference, never
16745
+ * a requirement.
16503
16746
  */
16504
- section: string().optional(),
16505
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16506
- sectionLabel: string().optional()
16747
+ mode: _enum(["navigate", "background"]).optional()
16507
16748
  });
16508
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
16509
- var AddonHttpRouteSchema = object({
16510
- method: _enum([
16511
- "GET",
16512
- "POST",
16513
- "PUT",
16514
- "DELETE",
16515
- "PATCH"
16749
+ /** A delivery parameter value: what a notifier declared it takes. */
16750
+ var DeliveryParamValueSchema = union([
16751
+ string(),
16752
+ number(),
16753
+ boolean()
16754
+ ]);
16755
+ /**
16756
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16757
+ * addresses a target of that kind may set it; a kind that declares nothing is
16758
+ * never handed one. The admin UI derives its control from this descriptor —
16759
+ * no editor knows a service by name.
16760
+ *
16761
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16762
+ * run on the addon cap path).
16763
+ */
16764
+ var DeliveryParamSchema = object({
16765
+ key: string().min(1),
16766
+ label: string(),
16767
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16768
+ labelKey: string().optional(),
16769
+ type: _enum([
16770
+ "enum",
16771
+ "number",
16772
+ "boolean",
16773
+ "string"
16516
16774
  ]),
16517
- path: string(),
16518
- access: _enum([
16519
- "public",
16520
- "authenticated",
16521
- "admin"
16522
- ]).optional(),
16523
- description: string().optional(),
16524
- /** The hub limits this route per client address before the handler runs. */
16525
- rateLimit: _enum(["auth"]).optional()
16775
+ /** `enum` only: the closed set of accepted values. */
16776
+ options: array(object({
16777
+ value: string(),
16778
+ label: string()
16779
+ })).optional(),
16780
+ /** `number` only. */
16781
+ min: number().optional(),
16782
+ max: number().optional(),
16783
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16784
+ default: DeliveryParamValueSchema.optional()
16526
16785
  });
16527
16786
  /**
16528
- * Cross-process route invocation envelope. The hub captures the
16529
- * request as plain data, ships it to the worker via Moleculer, and
16530
- * the worker runs the local handler against a capturing reply. The
16531
- * envelope returned describes what the handler intended (status,
16532
- * headers, body, or a redirect) so the hub can translate it back to
16533
- * the Fastify reply that's actually wired to the socket.
16787
+ * The canonical notification. `body` is the only hard field (Apprise model).
16788
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16789
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16790
+ * the adapter maps this ordinal onto its native level. `level?` is an
16791
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16792
+ * `priority` for that one target.
16534
16793
  */
16535
- var InvokeRequestSchema = object({
16536
- method: string(),
16537
- path: string(),
16538
- params: record(string(), string()),
16539
- query: record(string(), string()),
16540
- body: unknown(),
16541
- headers: record(string(), string()),
16542
- user: object({
16543
- id: string(),
16544
- username: string(),
16794
+ var NotificationSchema = object({
16795
+ body: string(),
16796
+ title: string().optional(),
16797
+ format: NotificationFormatSchema.default("text"),
16798
+ priority: number().int().min(1).max(5).default(3),
16799
+ level: string().optional(),
16800
+ attachments: array(AttachmentSchema).optional(),
16801
+ clickUrl: string().optional(),
16802
+ actions: array(NotificationActionSchema).optional(),
16803
+ sound: string().optional(),
16804
+ ttl: number().optional(),
16805
+ tag: string().optional(),
16806
+ deviceId: number().optional(),
16807
+ eventId: string().optional(),
16808
+ metadata: record(string(), unknown()).optional(),
16809
+ /**
16810
+ * Values for the parameters the target's kind DECLARES in
16811
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16812
+ * declaration by the sender; a notifier reads only the keys it declared.
16813
+ */
16814
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16815
+ });
16816
+ /** One declared native severity/priority level for a kind. */
16817
+ var TargetKindLevelSchema = object({
16818
+ id: string(),
16819
+ label: string(),
16820
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16821
+ ordinal: number().int().min(1).max(5).nullable(),
16822
+ flags: object({
16823
+ critical: boolean().optional(),
16824
+ silent: boolean().optional(),
16825
+ noPush: boolean().optional()
16826
+ }).optional(),
16827
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16828
+ requires: array(string()).optional(),
16829
+ description: string().optional()
16830
+ });
16831
+ /** The full capability block consulted before dispatch. */
16832
+ var TargetKindCapsSchema = object({
16833
+ attachments: object({
16834
+ mediaTypes: array(AttachmentMediaTypeSchema),
16835
+ mode: _enum([
16836
+ "url",
16837
+ "bytes",
16838
+ "both"
16839
+ ]),
16840
+ max: number().int().nonnegative(),
16841
+ maxBytes: number().int().positive().optional()
16842
+ }),
16843
+ /** Max action buttons (0 = none). */
16844
+ actions: number().int().nonnegative(),
16845
+ /**
16846
+ * Whether this kind renders a per-action ICON.
16847
+ *
16848
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16849
+ * run on the addon cap path — three production failures in one day taught
16850
+ * this repo that once. Absent is read as false by the degrade engine, which
16851
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16852
+ * assumed and dropped costs the operator's trust in the field.
16853
+ */
16854
+ actionIcons: boolean().optional(),
16855
+ levels: array(TargetKindLevelSchema),
16856
+ format: array(NotificationFormatSchema),
16857
+ clickUrl: boolean(),
16858
+ sound: boolean(),
16859
+ ttl: boolean(),
16860
+ bodyMaxLen: number().int().positive()
16861
+ });
16862
+ /**
16863
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16864
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16865
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16866
+ * the union is large and not meant for runtime validation here; the exported
16867
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16868
+ */
16869
+ var ConfigSchemaPassthrough$3 = unknown();
16870
+ var TargetKindSchema = object({
16871
+ kind: string(),
16872
+ label: string(),
16873
+ icon: string(),
16874
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16875
+ addonId: string(),
16876
+ /**
16877
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16878
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16879
+ * when the addon bundles no icon for that kind — the client then falls back
16880
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16881
+ *
16882
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16883
+ * client, and a native client joins it onto its own hub base.
16884
+ *
16885
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16886
+ * field that survived only because the runtime cap-router forwards provider
16887
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16888
+ * its own Zod parse from stripping it, and the whole arrangement would have
16889
+ * broken silently the moment output validation was tightened anywhere.
16890
+ */
16891
+ iconUrl: string().optional(),
16892
+ /**
16893
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16894
+ *
16895
+ * The server knows this and therefore says it, because the client cannot
16896
+ * safely guess: a React-Native client renders SVG and raster through two
16897
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16898
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16899
+ * placeholder glyph for every vector icon while the web build looked fine.
16900
+ *
16901
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16902
+ * not been updated — a client that cannot determine the type should prefer
16903
+ * its raster path, which is the safe default for an unknown image.
16904
+ */
16905
+ iconMediaType: string().optional(),
16906
+ configSchema: ConfigSchemaPassthrough$3,
16907
+ /**
16908
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16909
+ * base URL, instance, chat, recipient). A save that changes any of them
16910
+ * while leaving a secret redacted is refused by the provider (D634), so an
16911
+ * editor uses this to ask for the secret on exactly those edits instead of
16912
+ * on every edit.
16913
+ *
16914
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16915
+ * kinds) and from a provider older than this field — an editor that finds
16916
+ * it absent must assume ANY edit may need the secret.
16917
+ */
16918
+ destinationKeys: array(string()).optional(),
16919
+ supportsDiscovery: boolean(),
16920
+ /**
16921
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16922
+ * Absent = none; the rule editor then offers nothing.
16923
+ */
16924
+ deliveryParams: array(DeliveryParamSchema).optional(),
16925
+ caps: TargetKindCapsSchema
16926
+ });
16927
+ /**
16928
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16929
+ * (return a presence marker only) when serving `listTargets` — never
16930
+ * round-trip a stored secret to the UI.
16931
+ */
16932
+ var TargetSchema = object({
16933
+ id: string(),
16934
+ name: string(),
16935
+ kind: string(),
16936
+ addonId: string(),
16937
+ enabled: boolean(),
16938
+ config: record(string(), unknown())
16939
+ });
16940
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16941
+ var DiscoveredTargetSchema = object({
16942
+ kind: string(),
16943
+ suggestedName: string(),
16944
+ config: record(string(), unknown())
16945
+ });
16946
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16947
+ var RenderedAsSchema = object({
16948
+ level: string(),
16949
+ format: NotificationFormatSchema,
16950
+ attachmentsSent: number().int().nonnegative(),
16951
+ actionsSent: number().int().nonnegative(),
16952
+ truncated: boolean(),
16953
+ dropped: array(string())
16954
+ });
16955
+ var SendResultSchema = object({
16956
+ success: boolean(),
16957
+ error: string().optional(),
16958
+ renderedAs: RenderedAsSchema.optional()
16959
+ });
16960
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16961
+ var TestResultSchema = SendResultSchema;
16962
+ /**
16963
+ * Pre-save credential probe against a partial (unsaved) target config.
16964
+ * `resolvedUrl` never includes secrets (token stripped from path).
16965
+ */
16966
+ var ProbeTargetConfigResultSchema = object({
16967
+ ok: boolean(),
16968
+ resolvedUrl: string(),
16969
+ httpStatus: number().int().optional(),
16970
+ stateInstance: string().optional(),
16971
+ error: string().optional()
16972
+ });
16973
+ var notificationOutputCapability = {
16974
+ name: "notification-output",
16975
+ scope: "system",
16976
+ mode: "collection",
16977
+ methods: {
16978
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
16979
+ listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
16980
+ /**
16981
+ * One target WITH secrets — admin edit / duplicate hydration only.
16982
+ * `listTargets` stays redacted; the editor loads this so password fields
16983
+ * are filled (masked in the input) and a save can round-trip the token
16984
+ * when a destination changes (Duplicate with a new chatId, etc.).
16985
+ */
16986
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
16987
+ discoverTargets: method(object({
16988
+ kind: string(),
16989
+ config: record(string(), unknown()).optional(),
16990
+ /**
16991
+ * When editing / duplicating, the blank password field never round-trips
16992
+ * the stored token. Pass the source target id so the provider can restore
16993
+ * secrets server-side before dialling (same merge as upsert).
16994
+ */
16995
+ targetId: string().optional()
16996
+ }), array(DiscoveredTargetSchema), { auth: "admin" }),
16997
+ probeTargetConfig: method(object({
16998
+ kind: string(),
16999
+ config: record(string(), unknown()),
17000
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
17001
+ targetId: string().optional()
17002
+ }), ProbeTargetConfigResultSchema, {
17003
+ kind: "mutation",
17004
+ auth: "admin"
17005
+ }),
17006
+ send: method(object({
17007
+ targetId: string(),
17008
+ notification: NotificationSchema
17009
+ }), SendResultSchema, {
17010
+ kind: "mutation",
17011
+ auth: "admin"
17012
+ }),
17013
+ testTarget: method(object({
17014
+ targetId: string(),
17015
+ sample: NotificationSchema.optional()
17016
+ }), TestResultSchema, {
17017
+ kind: "mutation",
17018
+ auth: "admin"
17019
+ }),
17020
+ upsertTarget: method(object({
17021
+ target: TargetSchema,
17022
+ /**
17023
+ * Duplicate flow: create a NEW target id while copying secrets from an
17024
+ * existing row. The UI never holds the stored token; blank / redacted
17025
+ * password fields are restored from this source (D634 still applies if
17026
+ * destinations diverge without a re-typed secret).
17027
+ */
17028
+ cloneSecretsFrom: string().optional()
17029
+ }), TargetSchema, {
17030
+ kind: "mutation",
17031
+ auth: "admin"
17032
+ }),
17033
+ deleteTarget: method(object({ targetId: string() }), _void(), {
17034
+ kind: "mutation",
17035
+ auth: "admin"
17036
+ }),
17037
+ setTargetEnabled: method(object({
17038
+ targetId: string(),
17039
+ enabled: boolean()
17040
+ }), _void(), {
17041
+ kind: "mutation",
17042
+ auth: "admin"
17043
+ })
17044
+ }
17045
+ };
17046
+ function errorText(err) {
17047
+ return err instanceof Error ? err.message : String(err);
17048
+ }
17049
+ /** `actions.run` flattens a refusal to `[code] text`; the code leads. */
17050
+ function refusalFromThrown(message) {
17051
+ if (message.startsWith("[busy]")) return "already-running";
17052
+ if (message.startsWith("[not-found]")) return "not-found";
17053
+ return null;
17054
+ }
17055
+ async function runExportedAction(api, log, input) {
17056
+ const { exporter, actionId } = input;
17057
+ const tags = { actionId };
17058
+ const refuse = (reason, detail) => {
17059
+ log.warn(`${exporter}: exported action not run — ${reason}`, {
17060
+ tags,
17061
+ meta: {
17062
+ reason,
17063
+ ...detail ? { detail } : {}
17064
+ }
17065
+ });
17066
+ return {
17067
+ status: "refused",
17068
+ reason
17069
+ };
17070
+ };
17071
+ try {
17072
+ const result = await api.actions.run.mutate({ actionId });
17073
+ if (result.outcome === "skipped-overlap") return refuse("already-running", result.message);
17074
+ if (result.outcome === "skipped-cooldown") return refuse("cooldown", result.message);
17075
+ if (result.outcome === "failed" || result.outcome === "aborted") {
17076
+ const message = result.message ?? result.outcome;
17077
+ log.warn(`${exporter}: exported action failed`, {
17078
+ tags,
17079
+ meta: {
17080
+ outcome: result.outcome,
17081
+ message
17082
+ }
17083
+ });
17084
+ return {
17085
+ status: "failed",
17086
+ message
17087
+ };
17088
+ }
17089
+ log.info(`${exporter}: exported action ran`, {
17090
+ tags,
17091
+ meta: { outcome: result.outcome }
17092
+ });
17093
+ return { status: "started" };
17094
+ } catch (err) {
17095
+ const message = errorText(err);
17096
+ const reason = refusalFromThrown(message);
17097
+ if (reason !== null) return refuse(reason, message);
17098
+ log.warn(`${exporter}: exported action failed`, {
17099
+ tags,
17100
+ meta: { message }
17101
+ });
17102
+ return {
17103
+ status: "failed",
17104
+ message
17105
+ };
17106
+ }
17107
+ }
17108
+ /** Send through one CamStack notification target on behalf of an exporter. */
17109
+ async function sendToExportedTarget(api, log, input) {
17110
+ const { exporter, targetId } = input;
17111
+ const tags = { targetId };
17112
+ try {
17113
+ const target = (await api.notificationOutput.listTargets.query({})).find((t) => t.id === targetId);
17114
+ if (target === void 0) {
17115
+ log.warn(`${exporter}: exported notifier not used — target-gone`, {
17116
+ tags,
17117
+ meta: { reason: "target-gone" }
17118
+ });
17119
+ return {
17120
+ status: "refused",
17121
+ reason: "target-gone"
17122
+ };
17123
+ }
17124
+ if (!target.enabled) {
17125
+ log.warn(`${exporter}: exported notifier not used — target-disabled`, {
17126
+ tags,
17127
+ meta: { reason: "target-disabled" }
17128
+ });
17129
+ return {
17130
+ status: "refused",
17131
+ reason: "target-disabled"
17132
+ };
17133
+ }
17134
+ const result = await api.notificationOutput.send.mutate({
17135
+ targetId,
17136
+ addonId: target.addonId,
17137
+ notification: NotificationSchema.parse(input.notification)
17138
+ });
17139
+ if (!result.success) {
17140
+ const message = result.error ?? "send failed";
17141
+ log.warn(`${exporter}: exported notifier send failed`, {
17142
+ tags,
17143
+ meta: { message }
17144
+ });
17145
+ return {
17146
+ status: "failed",
17147
+ message
17148
+ };
17149
+ }
17150
+ return { status: "sent" };
17151
+ } catch (err) {
17152
+ const message = errorText(err);
17153
+ log.warn(`${exporter}: exported notifier send failed`, {
17154
+ tags,
17155
+ meta: { message }
17156
+ });
17157
+ return {
17158
+ status: "failed",
17159
+ message
17160
+ };
17161
+ }
17162
+ }
17163
+ /**
17164
+ * Split an exporter's selection into what still exists and what was deleted.
17165
+ * Devices are never judged here (their lifecycle is the tombstone, D865).
17166
+ * Callers pass ids from a query that ANSWERED — an unanswered list must not
17167
+ * reach this function (D49: a failed read changes nothing).
17168
+ */
17169
+ function pruneDeletedEntities(selection, live, log, exporter) {
17170
+ const kept = [];
17171
+ const dropped = [];
17172
+ for (const ref of selection) if (ref.kind === "action" && !live.actionIds.has(ref.actionId)) {
17173
+ dropped.push(ref);
17174
+ log.warn(`${exporter}: exported action was deleted — dropped from the export`, { tags: { actionId: ref.actionId } });
17175
+ } else if (ref.kind === "notifier" && !live.targetIds.has(ref.targetId)) {
17176
+ dropped.push(ref);
17177
+ log.warn(`${exporter}: exported notifier target was deleted — dropped from the export`, { tags: { targetId: ref.targetId } });
17178
+ } else kept.push(ref);
17179
+ return {
17180
+ kept,
17181
+ dropped
17182
+ };
17183
+ }
17184
+ /**
17185
+ * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
17186
+ * surface that admin-ui consumes through `useAddonPagesListPages()`.
17187
+ *
17188
+ * The provider iterates every `addon-pages-source` (collection) provider
17189
+ * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
17190
+ * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
17191
+ * filesystem `mtime` cache-buster lets the browser pick up addon
17192
+ * rebuilds without manual reload.
17193
+ *
17194
+ * The hub-local builtin `addon-pages-aggregator` (see
17195
+ * `@camstack/system/builtins/addon-pages-aggregator`) registers the
17196
+ * provider. Splitting the public aggregator from the raw collection
17197
+ * keeps both ends in codegen — there's no hand-written
17198
+ * `addon-pages.router.ts` wrapper anymore.
17199
+ */
17200
+ var AddonPageDeclarationSchema$1 = object({
17201
+ id: string(),
17202
+ label: string(),
17203
+ icon: string(),
17204
+ path: string(),
17205
+ remoteName: string(),
17206
+ bundle: string(),
17207
+ section: string().optional(),
17208
+ sectionLabel: string().optional()
17209
+ });
17210
+ var AddonPageInfoSchema = object({
17211
+ addonId: string(),
17212
+ page: AddonPageDeclarationSchema$1,
17213
+ bundleUrl: string()
17214
+ });
17215
+ method(_void(), array(AddonPageInfoSchema).readonly());
17216
+ var AddonHttpRouteSchema = object({
17217
+ method: _enum([
17218
+ "GET",
17219
+ "POST",
17220
+ "PUT",
17221
+ "DELETE",
17222
+ "PATCH"
17223
+ ]),
17224
+ path: string(),
17225
+ access: _enum([
17226
+ "public",
17227
+ "authenticated",
17228
+ "admin"
17229
+ ]).optional(),
17230
+ description: string().optional(),
17231
+ /** The hub limits this route per client address before the handler runs. */
17232
+ rateLimit: _enum(["auth"]).optional()
17233
+ });
17234
+ /**
17235
+ * Cross-process route invocation envelope. The hub captures the
17236
+ * request as plain data, ships it to the worker via Moleculer, and
17237
+ * the worker runs the local handler against a capturing reply. The
17238
+ * envelope returned describes what the handler intended (status,
17239
+ * headers, body, or a redirect) so the hub can translate it back to
17240
+ * the Fastify reply that's actually wired to the socket.
17241
+ */
17242
+ var InvokeRequestSchema = object({
17243
+ method: string(),
17244
+ path: string(),
17245
+ params: record(string(), string()),
17246
+ query: record(string(), string()),
17247
+ body: unknown(),
17248
+ headers: record(string(), string()),
17249
+ user: object({
17250
+ id: string(),
17251
+ username: string(),
16545
17252
  isAdmin: boolean()
16546
17253
  }).optional(),
16547
17254
  scopedToken: unknown().optional()
@@ -16806,9 +17513,21 @@ var AlertSeveritySchema = _enum([
16806
17513
  "error",
16807
17514
  "critical"
16808
17515
  ]);
17516
+ /**
17517
+ * An action that RUNS something instead of going somewhere: the admin UI calls
17518
+ * the addon's custom action (`addons.custom`) with exactly these fields. The
17519
+ * producer owns the meaning; the UI knows nothing about the addon or the kind.
17520
+ */
17521
+ var AlertActionInvokeSchema = object({
17522
+ addonId: string().min(1),
17523
+ action: string().min(1),
17524
+ input: record(string(), unknown()).optional()
17525
+ });
17526
+ /** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
16809
17527
  var AlertActionSchema = object({
16810
17528
  label: string(),
16811
17529
  href: string().optional(),
17530
+ invoke: AlertActionInvokeSchema.optional(),
16812
17531
  /**
16813
17532
  * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
16814
17533
  * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
@@ -19210,7 +19929,11 @@ var CompositionProblemSchema = object({
19210
19929
  "mixed-quantity",
19211
19930
  "helper-mismatch",
19212
19931
  "member-constraint",
19213
- "retention"
19932
+ "retention",
19933
+ "command-method-unknown",
19934
+ "command-source-unknown",
19935
+ "command-input-unknown",
19936
+ "command-input-missing"
19214
19937
  ]),
19215
19938
  path: string(),
19216
19939
  message: string()
@@ -19249,7 +19972,9 @@ var CompositionFieldRoleSchema = _enum([
19249
19972
  var CompositionFieldItemSchema = object({
19250
19973
  arrayPath: string(),
19251
19974
  key: string(),
19252
- label: string()
19975
+ label: string(),
19976
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19977
+ resettable: boolean().optional()
19253
19978
  });
19254
19979
  var CompositionFieldPlanSchema = object({
19255
19980
  cap: string(),
@@ -19454,7 +20179,7 @@ Object.freeze({
19454
20179
  * One catalog row per base reading a group helper can build (D944 §3).
19455
20180
  * Data, not a form default written into the block.
19456
20181
  */
19457
- var NOW$2 = {
20182
+ var NOW$3 = {
19458
20183
  fieldPath: "lastFetchedAt",
19459
20184
  expr: "now"
19460
20185
  };
@@ -19506,7 +20231,7 @@ var GROUP_TEMPLATES = [
19506
20231
  quantity: "temperature",
19507
20232
  defaultAggregate: "mean",
19508
20233
  empty: "null",
19509
- companions: [NOW$2],
20234
+ companions: [NOW$3],
19510
20235
  alsoFrom: {
19511
20236
  deviceTypes: CLIMATE_DEVICES,
19512
20237
  accept: {
@@ -19524,7 +20249,7 @@ var GROUP_TEMPLATES = [
19524
20249
  quantity: "humidity",
19525
20250
  defaultAggregate: "mean",
19526
20251
  empty: "null",
19527
- companions: [NOW$2],
20252
+ companions: [NOW$3],
19528
20253
  alsoFrom: {
19529
20254
  deviceTypes: CLIMATE_DEVICES,
19530
20255
  accept: {
@@ -19542,7 +20267,7 @@ var GROUP_TEMPLATES = [
19542
20267
  quantity: "pressure",
19543
20268
  defaultAggregate: "mean",
19544
20269
  empty: "null",
19545
- companions: [NOW$2]
20270
+ companions: [NOW$3]
19546
20271
  }),
19547
20272
  numeric({
19548
20273
  id: "illuminance",
@@ -19553,7 +20278,7 @@ var GROUP_TEMPLATES = [
19553
20278
  quantity: "illuminance",
19554
20279
  defaultAggregate: "mean",
19555
20280
  empty: "null",
19556
- companions: [NOW$2]
20281
+ companions: [NOW$3]
19557
20282
  }),
19558
20283
  numeric({
19559
20284
  id: "power",
@@ -19564,7 +20289,7 @@ var GROUP_TEMPLATES = [
19564
20289
  quantity: "power",
19565
20290
  defaultAggregate: "sum",
19566
20291
  empty: "omit",
19567
- companions: [NOW$2]
20292
+ companions: [NOW$3]
19568
20293
  }),
19569
20294
  numeric({
19570
20295
  id: "energy",
@@ -19575,7 +20300,7 @@ var GROUP_TEMPLATES = [
19575
20300
  quantity: "energy",
19576
20301
  defaultAggregate: "sum",
19577
20302
  empty: "omit",
19578
- companions: [NOW$2]
20303
+ companions: [NOW$3]
19579
20304
  }),
19580
20305
  numeric({
19581
20306
  id: "battery",
@@ -19610,7 +20335,7 @@ var GROUP_TEMPLATES = [
19610
20335
  quantity: "device",
19611
20336
  defaultAggregate: "mean",
19612
20337
  empty: "null",
19613
- companions: [NOW$2]
20338
+ companions: [NOW$3]
19614
20339
  }),
19615
20340
  binary({
19616
20341
  id: "motion",
@@ -22305,6 +23030,36 @@ var ExposeInputSchema = object({
22305
23030
  capabilities: array(string()).optional()
22306
23031
  });
22307
23032
  var UnexposeInputSchema = object({ deviceId: string() });
23033
+ /**
23034
+ * What an exporter may expose besides devices (D968). An action and a
23035
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
23036
+ * exporter's own selection, not the device registry.
23037
+ */
23038
+ var ExportEntityKindSchema = _enum([
23039
+ "device",
23040
+ "action",
23041
+ "notifier"
23042
+ ]);
23043
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
23044
+ object({
23045
+ kind: literal("device"),
23046
+ deviceId: string()
23047
+ }),
23048
+ object({
23049
+ kind: literal("action"),
23050
+ actionId: string().min(1)
23051
+ }),
23052
+ object({
23053
+ kind: literal("notifier"),
23054
+ targetId: string().min(1)
23055
+ })
23056
+ ]);
23057
+ var ExposedEntitySchema = object({
23058
+ entity: ExportEntityRefSchema,
23059
+ exposedAs: string().optional()
23060
+ });
23061
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
23062
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22308
23063
  var deviceExportCapability = {
22309
23064
  name: "device-export",
22310
23065
  scope: "system",
@@ -22334,6 +23089,16 @@ var deviceExportCapability = {
22334
23089
  unexposeDevice: method(UnexposeInputSchema, _void(), {
22335
23090
  kind: "mutation",
22336
23091
  auth: "admin"
23092
+ }),
23093
+ listSupportedEntityKinds: optionalMethod(_void(), array(ExportEntityKindSchema)),
23094
+ listExposedEntities: optionalMethod(_void(), array(ExposedEntitySchema)),
23095
+ exposeEntity: optionalMethod(ExposeEntityInputSchema, _void(), {
23096
+ kind: "mutation",
23097
+ auth: "admin"
23098
+ }),
23099
+ unexposeEntity: optionalMethod(UnexposeEntityInputSchema, _void(), {
23100
+ kind: "mutation",
23101
+ auth: "admin"
22337
23102
  })
22338
23103
  }
22339
23104
  };
@@ -43557,6 +44322,14 @@ method(object({
43557
44322
  kind: "mutation",
43558
44323
  auth: "admin"
43559
44324
  });
44325
+ method(object({
44326
+ nodeId: string().min(1),
44327
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
44328
+ removedAt: number()
44329
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
44330
+ kind: "mutation",
44331
+ auth: "admin"
44332
+ });
43560
44333
  /**
43561
44334
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
43562
44335
  * so it can delete what it keeps for that device (D865).
@@ -44277,7 +45050,7 @@ var LlmProviderWidgetSchema = object({
44277
45050
  /** ConfigUISchema tree passed through untyped on the wire (the
44278
45051
  * notification-output `ConfigSchemaPassthrough` precedent at
44279
45052
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
44280
- var ConfigSchemaPassthrough$3 = unknown();
45053
+ var ConfigSchemaPassthrough$2 = unknown();
44281
45054
  var LlmProfileKindDescriptorSchema = object({
44282
45055
  kind: LlmProfileKindSchema,
44283
45056
  label: string(),
@@ -44286,9 +45059,9 @@ var LlmProfileKindDescriptorSchema = object({
44286
45059
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
44287
45060
  addonId: string(),
44288
45061
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
44289
- connectionSchema: ConfigSchemaPassthrough$3,
45062
+ connectionSchema: ConfigSchemaPassthrough$2,
44290
45063
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
44291
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
45064
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
44292
45065
  /** The kind's defaults; a model from `listModels` may override any of them. */
44293
45066
  features: LlmModelFeaturesSchema,
44294
45067
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44814,10 +45587,10 @@ var CpuBreakdownSchema = object({
44814
45587
  cores: number()
44815
45588
  });
44816
45589
  var MemoryInfoSchema = object({
44817
- percent: number(),
45590
+ percent: number().nullable(),
44818
45591
  totalBytes: number(),
44819
- usedBytes: number(),
44820
- availableBytes: number(),
45592
+ usedBytes: number().nullable(),
45593
+ availableBytes: number().nullable(),
44821
45594
  swapUsedBytes: number(),
44822
45595
  swapTotalBytes: number()
44823
45596
  });
@@ -45085,722 +45858,280 @@ var NodeLoadSeriesSchema = object({
45085
45858
  /** The fixed sampling cadence in force on the cluster, in ms. */
45086
45859
  cadenceMs: number(),
45087
45860
  /**
45088
- * Did the DURABLE tier contribute? `false` means the answer is the hot ring
45089
- * alone — an agent (which holds no table), or a store that refused.
45090
- * Reported because "the last hour" and "the last six hours" are different
45091
- * questions and an operator must not have to guess which was answered.
45092
- */
45093
- durable: boolean()
45094
- });
45095
- var GetLoadSeriesInputSchema = object({
45096
- /**
45097
- * The node whose series is wanted.
45098
- *
45099
- * NOT named `nodeId`: the generated cap router strips a top-level
45100
- * `nodeId` from every method input and uses it to ROUTE the call to
45101
- * that node's provider (`generated-cap-routers.ts`). A series target
45102
- * called `nodeId` would silently become a routing pin and never reach
45103
- * the provider. The hub holds every node it hears from, so the
45104
- * ordinary call is unpinned — answered by the hub, for any node.
45105
- */
45106
- forNodeId: string(),
45107
- /**
45108
- * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
45109
- * holds and receives only what it is missing, so seeding a live chart
45110
- * from this method cannot double a point already drawn.
45111
- */
45112
- sinceMs: number().optional(),
45113
- /**
45114
- * Most points the caller wants PER FUNCTION. The window is reduced to fit,
45115
- * preserving min and max per bucket.
45116
- *
45117
- * Absent means NO reduction — legitimate for a short window and a trap for a
45118
- * long one, which is why a chart passes its own pixel width.
45119
- */
45120
- maxPoints: number().int().positive().optional()
45121
- });
45122
- var SystemMetricsSchema = object({
45123
- cpuPercent: number(),
45124
- memoryPercent: number(),
45125
- memoryUsedMB: number(),
45126
- memoryTotalMB: number(),
45127
- diskPercent: number().optional(),
45128
- temperature: number().optional(),
45129
- gpuPercent: number().optional(),
45130
- gpuMemoryPercent: number().optional()
45131
- });
45132
- 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);
45133
- method(object({
45134
- sourceUrl: string(),
45135
- metadata: ModelConvertMetadataSchema,
45136
- targets: array(ConvertTargetSchema).min(1).readonly(),
45137
- calibrationRef: string().optional(),
45138
- sessionId: string().optional()
45139
- }), ConvertResultSchema, {
45140
- kind: "mutation",
45141
- auth: "admin",
45142
- timeoutMs: 6e5
45143
- });
45144
- method(object({
45145
- targetNodeId: string(),
45146
- modelId: string(),
45147
- format: _enum(MODEL_FORMATS),
45148
- entry: ModelCatalogEntrySchema
45149
- }), object({
45150
- ok: boolean(),
45151
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
45152
- sha256: string(),
45153
- bytes: number(),
45154
- /** The target node's modelsDir the artifact landed in. */
45155
- path: string()
45156
- }), {
45157
- kind: "mutation",
45158
- auth: "admin"
45159
- });
45160
- var MotionRegionSchema = object({
45161
- bbox: BoundingBoxSchema,
45162
- pixelCount: number(),
45163
- intensity: number()
45164
- });
45165
- var MotionAnalysisResultSchema = object({
45166
- detected: boolean(),
45167
- regionCount: number(),
45168
- /** Regions passing minArea filter — used for pipeline triggering. */
45169
- regions: array(MotionRegionSchema).readonly(),
45170
- /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
45171
- rawRegions: array(MotionRegionSchema).readonly(),
45172
- frameWidth: number(),
45173
- frameHeight: number(),
45174
- analysisMs: number()
45175
- });
45176
- DeviceType.Camera, method(object({
45177
- deviceId: number(),
45178
- frame: FrameInputSchema.optional(),
45179
- frameHandle: FrameHandleSchema.optional()
45180
- }), MotionAnalysisResultSchema, {
45181
- role: "admin",
45182
- kind: "mutation"
45183
- }), method(object({ deviceId: number() }), _void(), {
45184
- role: "admin",
45185
- kind: "mutation"
45186
- }), method(_void(), _void(), {
45187
- role: "admin",
45188
- kind: "mutation"
45189
- }), object({
45190
- deviceId: number(),
45191
- detected: boolean(),
45192
- regionCount: number()
45193
- });
45194
- /**
45195
- * `mqtt-broker` — broker-registry cap.
45196
- *
45197
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
45198
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
45199
- * and (b) the connection details a consumer addon needs to spin up
45200
- * its OWN `mqtt.js` client.
45201
- *
45202
- * Why: pub/sub routing over the system event-bus loses fidelity
45203
- * (callback shape, QoS guarantees, will/retain semantics) and adds
45204
- * refcount bookkeeping that addons would rather own themselves. The
45205
- * canonical consumer needs raw `mqtt.js`
45206
- * features anyway — give it the connection config, get out of the way.
45207
- *
45208
- * Consumer flow:
45209
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
45210
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
45211
- * client.subscribe('zigbee2mqtt/+')
45212
- *
45213
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
45214
- * cloud bridge). The "embedded" entry (when present) is just another
45215
- * broker in the registry — its lifecycle is owned by the addon that
45216
- * spawned it.
45217
- */
45218
- var BrokerKindSchema = _enum(["external", "embedded"]);
45219
- /**
45220
- * Broker live-probe status.
45221
- *
45222
- * - `connected` — last probe completed a clean CONNACK
45223
- * - `disconnected` — no probe has run yet (cold cache)
45224
- * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
45225
- * - `unreachable` — TCP connect timed out / refused
45226
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
45227
- */
45228
- var BrokerStatusSchema$1 = _enum([
45229
- "connected",
45230
- "disconnected",
45231
- "auth-failed",
45232
- "unreachable",
45233
- "tls-error"
45234
- ]);
45235
- var BrokerInfoSchema$1 = object({
45236
- id: string(),
45237
- name: string(),
45238
- url: string(),
45239
- kind: BrokerKindSchema,
45240
- status: BrokerStatusSchema$1,
45241
- latencyMs: number().nullable(),
45242
- error: string().optional(),
45243
- /** Embedded brokers only: number of MQTT clients currently connected. */
45244
- connectedClients: number().int().nonnegative().optional(),
45245
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
45246
- lastCheckedAt: number().optional()
45247
- });
45248
- /**
45249
- * Connection details — what a consumer needs to call
45250
- * `mqtt.connect(url, options)`. We split URL + credentials so the
45251
- * consumer can pass them as `mqtt.connect(url, { username, password })`
45252
- * instead of stuffing creds into the URL (which leaks them into logs).
45253
- */
45254
- var BrokerConnectionDetailsSchema = object({
45255
- url: string(),
45256
- username: string().optional(),
45257
- password: string().optional(),
45258
- /**
45259
- * Suggested prefix for `clientId`. Each consumer should suffix this
45260
- * with its own discriminator (addon id, instance id) so reconnects
45261
- * don't kick each other off (MQTT spec: clientId must be unique per
45262
- * broker).
45263
- */
45264
- clientIdPrefix: string().optional()
45265
- });
45266
- var AddBrokerInputSchema = object({
45267
- name: string().min(1),
45268
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45269
- username: string().optional(),
45270
- password: string().optional(),
45271
- clientIdPrefix: string().optional()
45272
- });
45273
- var AddBrokerResultSchema = object({ id: string() });
45274
- var IdInputSchema = object({ id: string() });
45275
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
45276
- ok: literal(true),
45277
- latencyMs: number()
45278
- }), object({
45279
- ok: literal(false),
45280
- error: string()
45281
- })]);
45282
- var StartEmbeddedInputSchema = object({
45283
- port: number().int().min(1).max(65535).default(1883),
45284
- /** Allow anonymous connect (no username/password). Default: false. */
45285
- allowAnonymous: boolean().default(false),
45286
- /** Optional shared username/password for clients. */
45287
- username: string().optional(),
45288
- password: string().optional()
45289
- });
45290
- var StartEmbeddedResultSchema = object({
45291
- id: string(),
45292
- url: string()
45293
- });
45294
- var StatusSchema = object({
45295
- brokerCount: number(),
45296
- embeddedRunning: boolean()
45297
- });
45298
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45299
- kind: "mutation",
45300
- auth: "admin"
45301
- }), method(IdInputSchema, _void(), {
45302
- kind: "mutation",
45303
- auth: "admin"
45304
- }), method(IdInputSchema, TestResultSchema$1, {
45305
- kind: "mutation",
45306
- auth: "admin"
45307
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45308
- kind: "mutation",
45309
- auth: "admin"
45310
- }), method(IdInputSchema, _void(), {
45311
- kind: "mutation",
45312
- auth: "admin"
45313
- }), method(_void(), StatusSchema);
45314
- var NetworkEndpointSchema = object({
45315
- url: string(),
45316
- hostname: string(),
45317
- port: number(),
45318
- protocol: _enum(["http", "https"]),
45319
- /**
45320
- * Whether this address survives a restart of its provider. `false` = it is
45321
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45322
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
45323
- * it stays visible and an operator may still flag it. Absent = stable: every
45324
- * provider except a Quick tunnel owns a name that does not change.
45325
- */
45326
- stable: boolean().optional()
45327
- });
45328
- object({
45329
- url: string(),
45330
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45331
- stable: boolean().optional(),
45332
- updated: boolean().optional()
45333
- });
45334
- var NetworkAccessStatusSchema = object({
45335
- connected: boolean(),
45336
- endpoint: NetworkEndpointSchema.nullable(),
45337
- error: string().optional()
45338
- });
45339
- /**
45340
- * Optional, richer endpoint shape returned by providers that expose
45341
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
45342
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45343
- * the originating provider config (mode + sourcePort) so the
45344
- * orchestrator UI can label rows distinctly. Providers that expose only
45345
- * one endpoint just omit `listEndpoints` from their provider impl.
45346
- */
45347
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45348
- /**
45349
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
45350
- * the orchestrator can dedupe across `listEndpoints` polls.
45351
- */
45352
- id: string(),
45353
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45354
- label: string(),
45355
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45356
- mode: string().optional(),
45357
- /** Originating local port the ingress fronts (informational). */
45358
- sourcePort: number().optional()
45359
- });
45360
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45361
- /**
45362
- * notification-output — canonical, capability-gated notification delivery.
45363
- *
45364
- * Apprise-derived model (see
45365
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
45366
- * callers emit ONE canonical `Notification`; each provider declares a
45367
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
45368
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
45369
- * message to what the kind supports — callers never special-case a service.
45370
- *
45371
- * DESIGN DECISIONS (locked):
45372
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
45373
- * `setTargetEnabled`), each provider persisting via the `settings-store`
45374
- * cap. Rationale: the admin UI needs one uniform surface across the
45375
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
45376
- * alternative would fork the UI per addon and cannot host the
45377
- * discovery→adopt flow.
45378
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
45379
- * the generated cap-mount auto-`concatCollection`-fans them across every
45380
- * registered provider (notifiers addon + HA addon) so one catalog is
45381
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
45382
- * `addonId` the generated collection router extracts from the call input.
45383
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
45384
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
45385
- * `storage` / `storage-provider` / `recording` caps over the same path. No
45386
- * base64 fallback needed.
45387
- *
45388
- * TODO (deferred, closed-set change — separate decision): add
45389
- * `providerKind: 'notify'` so notification providers surface on the unified
45390
- * admin "Integrations" page.
45391
- */
45392
- /**
45393
- * Zentik-derived typed-media enum — the superset across every kind. Each
45394
- * adapter picks what it supports and the degrade engine filters the rest.
45395
- */
45396
- var AttachmentMediaTypeSchema = _enum([
45397
- "image",
45398
- "video",
45399
- "gif",
45400
- "audio",
45401
- "icon"
45402
- ]);
45403
- /**
45404
- * A single attachment. Exactly one of `url` (remote source, most adapters
45405
- * prefer this) or `bytes` (inline source; required for Pushover-style
45406
- * bytes-only kinds) MUST be present — the degrade engine expresses a
45407
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
45408
- */
45409
- var AttachmentSchema = object({
45410
- mediaType: AttachmentMediaTypeSchema,
45411
- url: string().optional(),
45412
- bytes: _instanceof(Uint8Array).optional(),
45413
- mime: string().optional(),
45414
- name: string().optional()
45415
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
45416
- var NotificationFormatSchema = _enum([
45417
- "text",
45418
- "markdown",
45419
- "html"
45420
- ]);
45421
- /**
45422
- * The CLOSED icon vocabulary an action button may use.
45423
- *
45424
- * A closed set, not a free string, and that is the whole point: an arbitrary
45425
- * icon name is one that ntfy renders, zentik silently drops, and nobody
45426
- * notices — the same class of gap as a zone vocabulary nothing produced
45427
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
45428
- * declares `actionIcons: false` and the degrade engine strips the field.
45429
- *
45430
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
45431
- * renderer's icon set; "acknowledge" survives an adapter that draws it
45432
- * differently.
45433
- *
45434
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
45435
- *
45436
- * These names are for US. **No adapter may forward one verbatim.** Each maps
45437
- * the whole set onto its own renderer's vocabulary through a
45438
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
45439
- * fallback, so adding a member here fails every adapter's build until someone
45440
- * decides its glyph, which is the only place that decision can be made
45441
- * honestly.
45442
- *
45443
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
45444
- * `disarm` straight through; iOS feeds that string to
45445
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
45446
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
45447
- * mapping, and "the field is documented" is not "the value renders".
45448
- *
45449
- * Adding a member is TRAIN-BOUND. The enum lives in the published
45450
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
45451
- * so an addon that emits a token the running hub does not know does not lose an
45452
- * icon — its whole `send` fails Zod validation and the notification never
45453
- * arrives. Never emit a new token from an addon before the train carrying it.
45454
- */
45455
- var NotificationActionIconSchema = _enum([
45456
- "acknowledge",
45457
- "dismiss",
45458
- "silence",
45459
- "snooze",
45460
- "view",
45461
- "play",
45462
- "open",
45463
- "close",
45464
- "lock",
45465
- "unlock",
45466
- "arm",
45467
- "arm-home",
45468
- "arm-away",
45469
- "arm-night",
45470
- "disarm",
45471
- "light",
45472
- "alert",
45473
- "camera"
45474
- ]);
45475
- /** A single tap-through action button. */
45476
- var NotificationActionSchema = object({
45477
- id: string(),
45478
- label: string(),
45479
- url: string().optional(),
45480
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
45481
- icon: NotificationActionIconSchema.optional(),
45482
- /**
45483
- * Renders in a warning style where the notifier supports it.
45484
- *
45485
- * A HINT, never a gate. The callback's authority is its token and nothing
45486
- * else — see `notification-center/action-token.ts` for what that does and
45487
- * does not buy.
45488
- */
45489
- destructive: boolean().optional(),
45490
- /**
45491
- * How the tap should REACH the url.
45492
- *
45493
- * `navigate` (absent, and every button authored before this field) opens it:
45494
- * the phone leaves the notification and shows whatever the callback returns.
45495
- * That is right for a button whose answer the operator wants to read.
45496
- *
45497
- * `background` fires it as a POST and stays put. It exists for the buttons
45498
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
45499
- * an answer to the notification, and being thrown into a browser tab to
45500
- * confirm it costs more attention than the notification did. A backend that
45501
- * cannot do a background call renders it as an ordinary link (the adapters
45502
- * fall back rather than dropping the button), so this is a preference, never
45503
- * a requirement.
45504
- */
45505
- mode: _enum(["navigate", "background"]).optional()
45506
- });
45507
- /** A delivery parameter value: what a notifier declared it takes. */
45508
- var DeliveryParamValueSchema = union([
45509
- string(),
45510
- number(),
45511
- boolean()
45512
- ]);
45513
- /**
45514
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
45515
- * addresses a target of that kind may set it; a kind that declares nothing is
45516
- * never handed one. The admin UI derives its control from this descriptor —
45517
- * no editor knows a service by name.
45518
- *
45519
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
45520
- * run on the addon cap path).
45521
- */
45522
- var DeliveryParamSchema = object({
45523
- key: string().min(1),
45524
- label: string(),
45525
- /** i18n key of the label, resolved by the declaring addon's namespace. */
45526
- labelKey: string().optional(),
45527
- type: _enum([
45528
- "enum",
45529
- "number",
45530
- "boolean",
45531
- "string"
45532
- ]),
45533
- /** `enum` only: the closed set of accepted values. */
45534
- options: array(object({
45535
- value: string(),
45536
- label: string()
45537
- })).optional(),
45538
- /** `number` only. */
45539
- min: number().optional(),
45540
- max: number().optional(),
45541
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
45542
- default: DeliveryParamValueSchema.optional()
45543
- });
45544
- /**
45545
- * The canonical notification. `body` is the only hard field (Apprise model).
45546
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
45547
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
45548
- * the adapter maps this ordinal onto its native level. `level?` is an
45549
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
45550
- * `priority` for that one target.
45551
- */
45552
- var NotificationSchema = object({
45553
- body: string(),
45554
- title: string().optional(),
45555
- format: NotificationFormatSchema.default("text"),
45556
- priority: number().int().min(1).max(5).default(3),
45557
- level: string().optional(),
45558
- attachments: array(AttachmentSchema).optional(),
45559
- clickUrl: string().optional(),
45560
- actions: array(NotificationActionSchema).optional(),
45561
- sound: string().optional(),
45562
- ttl: number().optional(),
45563
- tag: string().optional(),
45564
- deviceId: number().optional(),
45565
- eventId: string().optional(),
45566
- metadata: record(string(), unknown()).optional(),
45567
- /**
45568
- * Values for the parameters the target's kind DECLARES in
45569
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45570
- * declaration by the sender; a notifier reads only the keys it declared.
45571
- */
45572
- delivery: record(string(), DeliveryParamValueSchema).optional()
45573
- });
45574
- /** One declared native severity/priority level for a kind. */
45575
- var TargetKindLevelSchema = object({
45576
- id: string(),
45577
- label: string(),
45578
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
45579
- ordinal: number().int().min(1).max(5).nullable(),
45580
- flags: object({
45581
- critical: boolean().optional(),
45582
- silent: boolean().optional(),
45583
- noPush: boolean().optional()
45584
- }).optional(),
45585
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
45586
- requires: array(string()).optional(),
45587
- description: string().optional()
45588
- });
45589
- /** The full capability block consulted before dispatch. */
45590
- var TargetKindCapsSchema = object({
45591
- attachments: object({
45592
- mediaTypes: array(AttachmentMediaTypeSchema),
45593
- mode: _enum([
45594
- "url",
45595
- "bytes",
45596
- "both"
45597
- ]),
45598
- max: number().int().nonnegative(),
45599
- maxBytes: number().int().positive().optional()
45600
- }),
45601
- /** Max action buttons (0 = none). */
45602
- actions: number().int().nonnegative(),
45603
- /**
45604
- * Whether this kind renders a per-action ICON.
45605
- *
45606
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
45607
- * run on the addon cap path — three production failures in one day taught
45608
- * this repo that once. Absent is read as false by the degrade engine, which
45609
- * is the safe direction: an icon that is not rendered costs nothing, an icon
45610
- * assumed and dropped costs the operator's trust in the field.
45611
- */
45612
- actionIcons: boolean().optional(),
45613
- levels: array(TargetKindLevelSchema),
45614
- format: array(NotificationFormatSchema),
45615
- clickUrl: boolean(),
45616
- sound: boolean(),
45617
- ttl: boolean(),
45618
- bodyMaxLen: number().int().positive()
45619
- });
45620
- /**
45621
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
45622
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
45623
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
45624
- * the union is large and not meant for runtime validation here; the exported
45625
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
45626
- */
45627
- var ConfigSchemaPassthrough$2 = unknown();
45628
- var TargetKindSchema = object({
45629
- kind: string(),
45630
- label: string(),
45631
- icon: string(),
45632
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
45633
- addonId: string(),
45634
- /**
45635
- * URL of the kind's bundled BRAND icon, served by the providing addon over
45636
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
45637
- * when the addon bundles no icon for that kind — the client then falls back
45638
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
45639
- *
45640
- * Root-relative on purpose: it resolves against whatever origin serves a web
45641
- * client, and a native client joins it onto its own hub base.
45642
- *
45643
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
45644
- * field that survived only because the runtime cap-router forwards provider
45645
- * output verbatim — so every consumer had to re-declare it by hand to stop
45646
- * its own Zod parse from stripping it, and the whole arrangement would have
45647
- * broken silently the moment output validation was tightened anywhere.
45648
- */
45649
- iconUrl: string().optional(),
45650
- /**
45651
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
45652
- *
45653
- * The server knows this and therefore says it, because the client cannot
45654
- * safely guess: a React-Native client renders SVG and raster through two
45655
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
45656
- * not decode SVG on iOS/Android), so without this it silently fell back to a
45657
- * placeholder glyph for every vector icon while the web build looked fine.
45658
- *
45659
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
45660
- * not been updated — a client that cannot determine the type should prefer
45661
- * its raster path, which is the safe default for an unknown image.
45861
+ * Did the DURABLE tier contribute? `false` means the answer is the hot ring
45862
+ * alone — an agent (which holds no table), or a store that refused.
45863
+ * Reported because "the last hour" and "the last six hours" are different
45864
+ * questions and an operator must not have to guess which was answered.
45662
45865
  */
45663
- iconMediaType: string().optional(),
45664
- configSchema: ConfigSchemaPassthrough$2,
45866
+ durable: boolean()
45867
+ });
45868
+ var GetLoadSeriesInputSchema = object({
45665
45869
  /**
45666
- * The config keys that decide WHERE this kind's secrets are sent (server,
45667
- * base URL, instance, chat, recipient). A save that changes any of them
45668
- * while leaving a secret redacted is refused by the provider (D634), so an
45669
- * editor uses this to ask for the secret on exactly those edits instead of
45670
- * on every edit.
45870
+ * The node whose series is wanted.
45671
45871
  *
45672
- * Optional: absent from a provider that holds no secret (the Home Assistant
45673
- * kinds) and from a provider older than this field — an editor that finds
45674
- * it absent must assume ANY edit may need the secret.
45872
+ * NOT named `nodeId`: the generated cap router strips a top-level
45873
+ * `nodeId` from every method input and uses it to ROUTE the call to
45874
+ * that node's provider (`generated-cap-routers.ts`). A series target
45875
+ * called `nodeId` would silently become a routing pin and never reach
45876
+ * the provider. The hub holds every node it hears from, so the
45877
+ * ordinary call is unpinned — answered by the hub, for any node.
45675
45878
  */
45676
- destinationKeys: array(string()).optional(),
45677
- supportsDiscovery: boolean(),
45879
+ forNodeId: string(),
45678
45880
  /**
45679
- * Extra delivery parameters a rule may set for a target of this kind (D938).
45680
- * Absent = none; the rule editor then offers nothing.
45881
+ * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
45882
+ * holds and receives only what it is missing, so seeding a live chart
45883
+ * from this method cannot double a point already drawn.
45681
45884
  */
45682
- deliveryParams: array(DeliveryParamSchema).optional(),
45683
- caps: TargetKindCapsSchema
45885
+ sinceMs: number().optional(),
45886
+ /**
45887
+ * Most points the caller wants PER FUNCTION. The window is reduced to fit,
45888
+ * preserving min and max per bucket.
45889
+ *
45890
+ * Absent means NO reduction — legitimate for a short window and a trap for a
45891
+ * long one, which is why a chart passes its own pixel width.
45892
+ */
45893
+ maxPoints: number().int().positive().optional()
45894
+ });
45895
+ var SystemMetricsSchema = object({
45896
+ cpuPercent: number(),
45897
+ /** `null` when the memory sample failed or was not taken yet (D393). */
45898
+ memoryPercent: number().nullable(),
45899
+ memoryUsedMB: number().nullable(),
45900
+ memoryTotalMB: number(),
45901
+ diskPercent: number().optional(),
45902
+ temperature: number().optional(),
45903
+ gpuPercent: number().optional(),
45904
+ gpuMemoryPercent: number().optional()
45905
+ });
45906
+ 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);
45907
+ method(object({
45908
+ sourceUrl: string(),
45909
+ metadata: ModelConvertMetadataSchema,
45910
+ targets: array(ConvertTargetSchema).min(1).readonly(),
45911
+ calibrationRef: string().optional(),
45912
+ sessionId: string().optional()
45913
+ }), ConvertResultSchema, {
45914
+ kind: "mutation",
45915
+ auth: "admin",
45916
+ timeoutMs: 6e5
45917
+ });
45918
+ method(object({
45919
+ targetNodeId: string(),
45920
+ modelId: string(),
45921
+ format: _enum(MODEL_FORMATS),
45922
+ entry: ModelCatalogEntrySchema
45923
+ }), object({
45924
+ ok: boolean(),
45925
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
45926
+ sha256: string(),
45927
+ bytes: number(),
45928
+ /** The target node's modelsDir the artifact landed in. */
45929
+ path: string()
45930
+ }), {
45931
+ kind: "mutation",
45932
+ auth: "admin"
45933
+ });
45934
+ var MotionRegionSchema = object({
45935
+ bbox: BoundingBoxSchema,
45936
+ pixelCount: number(),
45937
+ intensity: number()
45938
+ });
45939
+ var MotionAnalysisResultSchema = object({
45940
+ detected: boolean(),
45941
+ regionCount: number(),
45942
+ /** Regions passing minArea filter — used for pipeline triggering. */
45943
+ regions: array(MotionRegionSchema).readonly(),
45944
+ /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
45945
+ rawRegions: array(MotionRegionSchema).readonly(),
45946
+ frameWidth: number(),
45947
+ frameHeight: number(),
45948
+ analysisMs: number()
45949
+ });
45950
+ DeviceType.Camera, method(object({
45951
+ deviceId: number(),
45952
+ frame: FrameInputSchema.optional(),
45953
+ frameHandle: FrameHandleSchema.optional()
45954
+ }), MotionAnalysisResultSchema, {
45955
+ role: "admin",
45956
+ kind: "mutation"
45957
+ }), method(object({ deviceId: number() }), _void(), {
45958
+ role: "admin",
45959
+ kind: "mutation"
45960
+ }), method(_void(), _void(), {
45961
+ role: "admin",
45962
+ kind: "mutation"
45963
+ }), object({
45964
+ deviceId: number(),
45965
+ detected: boolean(),
45966
+ regionCount: number()
45684
45967
  });
45685
45968
  /**
45686
- * A persisted target. `config` holds secrets; providers REDACT secret fields
45687
- * (return a presence marker only) when serving `listTargets` — never
45688
- * round-trip a stored secret to the UI.
45969
+ * `mqtt-broker` — broker-registry cap.
45970
+ *
45971
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
45972
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
45973
+ * and (b) the connection details a consumer addon needs to spin up
45974
+ * its OWN `mqtt.js` client.
45975
+ *
45976
+ * Why: pub/sub routing over the system event-bus loses fidelity
45977
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
45978
+ * refcount bookkeeping that addons would rather own themselves. The
45979
+ * canonical consumer needs raw `mqtt.js`
45980
+ * features anyway — give it the connection config, get out of the way.
45981
+ *
45982
+ * Consumer flow:
45983
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
45984
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
45985
+ * client.subscribe('zigbee2mqtt/+')
45986
+ *
45987
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
45988
+ * cloud bridge). The "embedded" entry (when present) is just another
45989
+ * broker in the registry — its lifecycle is owned by the addon that
45990
+ * spawned it.
45689
45991
  */
45690
- var TargetSchema = object({
45992
+ var BrokerKindSchema = _enum(["external", "embedded"]);
45993
+ /**
45994
+ * Broker live-probe status.
45995
+ *
45996
+ * - `connected` — last probe completed a clean CONNACK
45997
+ * - `disconnected` — no probe has run yet (cold cache)
45998
+ * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
45999
+ * - `unreachable` — TCP connect timed out / refused
46000
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
46001
+ */
46002
+ var BrokerStatusSchema$1 = _enum([
46003
+ "connected",
46004
+ "disconnected",
46005
+ "auth-failed",
46006
+ "unreachable",
46007
+ "tls-error"
46008
+ ]);
46009
+ var BrokerInfoSchema$1 = object({
45691
46010
  id: string(),
45692
46011
  name: string(),
45693
- kind: string(),
45694
- addonId: string(),
45695
- enabled: boolean(),
45696
- config: record(string(), unknown())
46012
+ url: string(),
46013
+ kind: BrokerKindSchema,
46014
+ status: BrokerStatusSchema$1,
46015
+ latencyMs: number().nullable(),
46016
+ error: string().optional(),
46017
+ /** Embedded brokers only: number of MQTT clients currently connected. */
46018
+ connectedClients: number().int().nonnegative().optional(),
46019
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
46020
+ lastCheckedAt: number().optional()
45697
46021
  });
45698
- /** A discovery-surfaced candidate (config is partial + non-secret). */
45699
- var DiscoveredTargetSchema = object({
45700
- kind: string(),
45701
- suggestedName: string(),
45702
- config: record(string(), unknown())
46022
+ /**
46023
+ * Connection details — what a consumer needs to call
46024
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
46025
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
46026
+ * instead of stuffing creds into the URL (which leaks them into logs).
46027
+ */
46028
+ var BrokerConnectionDetailsSchema = object({
46029
+ url: string(),
46030
+ username: string().optional(),
46031
+ password: string().optional(),
46032
+ /**
46033
+ * Suggested prefix for `clientId`. Each consumer should suffix this
46034
+ * with its own discriminator (addon id, instance id) so reconnects
46035
+ * don't kick each other off (MQTT spec: clientId must be unique per
46036
+ * broker).
46037
+ */
46038
+ clientIdPrefix: string().optional()
45703
46039
  });
45704
- /** The degrade engine's report — what was resolved / dropped / degraded. */
45705
- var RenderedAsSchema = object({
45706
- level: string(),
45707
- format: NotificationFormatSchema,
45708
- attachmentsSent: number().int().nonnegative(),
45709
- actionsSent: number().int().nonnegative(),
45710
- truncated: boolean(),
45711
- dropped: array(string())
46040
+ var AddBrokerInputSchema = object({
46041
+ name: string().min(1),
46042
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
46043
+ username: string().optional(),
46044
+ password: string().optional(),
46045
+ clientIdPrefix: string().optional()
45712
46046
  });
45713
- var SendResultSchema = object({
45714
- success: boolean(),
45715
- error: string().optional(),
45716
- renderedAs: RenderedAsSchema.optional()
46047
+ var AddBrokerResultSchema = object({ id: string() });
46048
+ var IdInputSchema = object({ id: string() });
46049
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
46050
+ ok: literal(true),
46051
+ latencyMs: number()
46052
+ }), object({
46053
+ ok: literal(false),
46054
+ error: string()
46055
+ })]);
46056
+ var StartEmbeddedInputSchema = object({
46057
+ port: number().int().min(1).max(65535).default(1883),
46058
+ /** Allow anonymous connect (no username/password). Default: false. */
46059
+ allowAnonymous: boolean().default(false),
46060
+ /** Optional shared username/password for clients. */
46061
+ username: string().optional(),
46062
+ password: string().optional()
46063
+ });
46064
+ var StartEmbeddedResultSchema = object({
46065
+ id: string(),
46066
+ url: string()
46067
+ });
46068
+ var StatusSchema = object({
46069
+ brokerCount: number(),
46070
+ embeddedRunning: boolean()
46071
+ });
46072
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
46073
+ kind: "mutation",
46074
+ auth: "admin"
46075
+ }), method(IdInputSchema, _void(), {
46076
+ kind: "mutation",
46077
+ auth: "admin"
46078
+ }), method(IdInputSchema, TestResultSchema$1, {
46079
+ kind: "mutation",
46080
+ auth: "admin"
46081
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
46082
+ kind: "mutation",
46083
+ auth: "admin"
46084
+ }), method(IdInputSchema, _void(), {
46085
+ kind: "mutation",
46086
+ auth: "admin"
46087
+ }), method(_void(), StatusSchema);
46088
+ var NetworkEndpointSchema = object({
46089
+ url: string(),
46090
+ hostname: string(),
46091
+ port: number(),
46092
+ protocol: _enum(["http", "https"]),
46093
+ /**
46094
+ * Whether this address survives a restart of its provider. `false` = it is
46095
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
46096
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
46097
+ * it stays visible and an operator may still flag it. Absent = stable: every
46098
+ * provider except a Quick tunnel owns a name that does not change.
46099
+ */
46100
+ stable: boolean().optional()
46101
+ });
46102
+ object({
46103
+ url: string(),
46104
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
46105
+ stable: boolean().optional(),
46106
+ updated: boolean().optional()
46107
+ });
46108
+ var NetworkAccessStatusSchema = object({
46109
+ connected: boolean(),
46110
+ endpoint: NetworkEndpointSchema.nullable(),
46111
+ error: string().optional()
45717
46112
  });
45718
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
45719
- var TestResultSchema = SendResultSchema;
45720
46113
  /**
45721
- * Pre-save credential probe against a partial (unsaved) target config.
45722
- * `resolvedUrl` never includes secrets (token stripped from path).
46114
+ * Optional, richer endpoint shape returned by providers that expose
46115
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
46116
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
46117
+ * the originating provider config (mode + sourcePort) so the
46118
+ * orchestrator UI can label rows distinctly. Providers that expose only
46119
+ * one endpoint just omit `listEndpoints` from their provider impl.
45723
46120
  */
45724
- var ProbeTargetConfigResultSchema = object({
45725
- ok: boolean(),
45726
- resolvedUrl: string(),
45727
- httpStatus: number().int().optional(),
45728
- stateInstance: string().optional(),
45729
- error: string().optional()
46121
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
46122
+ /**
46123
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
46124
+ * the orchestrator can dedupe across `listEndpoints` polls.
46125
+ */
46126
+ id: string(),
46127
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
46128
+ label: string(),
46129
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
46130
+ mode: string().optional(),
46131
+ /** Originating local port the ingress fronts (informational). */
46132
+ sourcePort: number().optional()
45730
46133
  });
45731
- var notificationOutputCapability = {
45732
- name: "notification-output",
45733
- scope: "system",
45734
- mode: "collection",
45735
- methods: {
45736
- listTargetKinds: method(object({}), array(TargetKindSchema)),
45737
- listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
45738
- /**
45739
- * One target WITH secrets — admin edit / duplicate hydration only.
45740
- * `listTargets` stays redacted; the editor loads this so password fields
45741
- * are filled (masked in the input) and a save can round-trip the token
45742
- * when a destination changes (Duplicate with a new chatId, etc.).
45743
- */
45744
- getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
45745
- discoverTargets: method(object({
45746
- kind: string(),
45747
- config: record(string(), unknown()).optional(),
45748
- /**
45749
- * When editing / duplicating, the blank password field never round-trips
45750
- * the stored token. Pass the source target id so the provider can restore
45751
- * secrets server-side before dialling (same merge as upsert).
45752
- */
45753
- targetId: string().optional()
45754
- }), array(DiscoveredTargetSchema), { auth: "admin" }),
45755
- probeTargetConfig: method(object({
45756
- kind: string(),
45757
- config: record(string(), unknown()),
45758
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
45759
- targetId: string().optional()
45760
- }), ProbeTargetConfigResultSchema, {
45761
- kind: "mutation",
45762
- auth: "admin"
45763
- }),
45764
- send: method(object({
45765
- targetId: string(),
45766
- notification: NotificationSchema
45767
- }), SendResultSchema, {
45768
- kind: "mutation",
45769
- auth: "admin"
45770
- }),
45771
- testTarget: method(object({
45772
- targetId: string(),
45773
- sample: NotificationSchema.optional()
45774
- }), TestResultSchema, {
45775
- kind: "mutation",
45776
- auth: "admin"
45777
- }),
45778
- upsertTarget: method(object({
45779
- target: TargetSchema,
45780
- /**
45781
- * Duplicate flow: create a NEW target id while copying secrets from an
45782
- * existing row. The UI never holds the stored token; blank / redacted
45783
- * password fields are restored from this source (D634 still applies if
45784
- * destinations diverge without a re-typed secret).
45785
- */
45786
- cloneSecretsFrom: string().optional()
45787
- }), TargetSchema, {
45788
- kind: "mutation",
45789
- auth: "admin"
45790
- }),
45791
- deleteTarget: method(object({ targetId: string() }), _void(), {
45792
- kind: "mutation",
45793
- auth: "admin"
45794
- }),
45795
- setTargetEnabled: method(object({
45796
- targetId: string(),
45797
- enabled: boolean()
45798
- }), _void(), {
45799
- kind: "mutation",
45800
- auth: "admin"
45801
- })
45802
- }
45803
- };
46134
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45804
46135
  var AUDIO_MACRO_LABELS = [
45805
46136
  {
45806
46137
  id: "speech",
@@ -52261,14 +52592,6 @@ var EngineProvisioningSchema = object({
52261
52592
  */
52262
52593
  configIssues: array(string()).optional()
52263
52594
  });
52264
- var PipelineStepInputSchema = lazy(() => object({
52265
- addonId: string(),
52266
- modelId: string().optional(),
52267
- enabled: boolean().default(true),
52268
- children: array(PipelineStepInputSchema).optional(),
52269
- settings: record(string(), unknown()).optional(),
52270
- jumpDeviceKey: string().optional()
52271
- }));
52272
52595
  var ModelSubstitutionSchema = object({
52273
52596
  addonId: string(),
52274
52597
  chosen: string(),
@@ -52347,7 +52670,6 @@ var ConfigUISchemaBridge = custom();
52347
52670
  var ConfigUISchemaNullableBridge = custom();
52348
52671
  var InferenceCapabilitiesBridge = custom();
52349
52672
  var ModelAvailabilityListBridge = custom();
52350
- var PipelineRunResultBridge = custom();
52351
52673
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
52352
52674
  modelId: string(),
52353
52675
  settings: record(string(), unknown()).readonly()
@@ -59213,6 +59535,318 @@ onBrightnessChanged: { data: object({
59213
59535
  } } }
59214
59536
  };
59215
59537
  /**
59538
+ * The role vocabulary of a remote (D963): what a button MEANS, independent of
59539
+ * how it is wired. A layout template places buttons by role; a device binds a
59540
+ * role to an action; an exporter asks for a role by name. Roles are shared so
59541
+ * one layout fits every appliance and one voice command fits every binding.
59542
+ *
59543
+ * A role outside this list is `custom:<key>` — the operator's own button (a
59544
+ * SmartIR source, a channel preset). The key is a lowercase slug.
59545
+ */
59546
+ var REMOTE_KNOWN_ROLES = [
59547
+ "power",
59548
+ "power_on",
59549
+ "power_off",
59550
+ "volume_up",
59551
+ "volume_down",
59552
+ "mute",
59553
+ "channel_up",
59554
+ "channel_down",
59555
+ "up",
59556
+ "down",
59557
+ "left",
59558
+ "right",
59559
+ "ok",
59560
+ "back",
59561
+ "home",
59562
+ "menu",
59563
+ "info",
59564
+ "guide",
59565
+ "exit",
59566
+ "input",
59567
+ "play",
59568
+ "pause",
59569
+ "play_pause",
59570
+ "stop",
59571
+ "next",
59572
+ "previous",
59573
+ "fast_forward",
59574
+ "rewind",
59575
+ "digit_0",
59576
+ "digit_1",
59577
+ "digit_2",
59578
+ "digit_3",
59579
+ "digit_4",
59580
+ "digit_5",
59581
+ "digit_6",
59582
+ "digit_7",
59583
+ "digit_8",
59584
+ "digit_9"
59585
+ ];
59586
+ var CUSTOM_KEY_RE = /^[a-z0-9][a-z0-9_]{0,47}$/;
59587
+ var KNOWN_ROLES = new Set(REMOTE_KNOWN_ROLES);
59588
+ function isRemoteKnownRole(value) {
59589
+ return KNOWN_ROLES.has(value);
59590
+ }
59591
+ function isRemoteCustomKey(key) {
59592
+ return CUSTOM_KEY_RE.test(key);
59593
+ }
59594
+ /** The key of a `custom:<key>` role, or null for any other string. */
59595
+ function customKeyOfRole(role) {
59596
+ if (!role.startsWith("custom:")) return null;
59597
+ const key = role.slice(7);
59598
+ return isRemoteCustomKey(key) ? key : null;
59599
+ }
59600
+ function isRemoteRole(value) {
59601
+ return isRemoteKnownRole(value) || customKeyOfRole(value) !== null;
59602
+ }
59603
+ var RemoteRoleSchema = string().refine(isRemoteRole, { message: "a role is one of the known roles or custom:<key> (lowercase letters, digits, _)" });
59604
+ var RemoteBindingSchema = object({ action: AutomationCapStepSchema.extend({ deviceId: number().int().nonnegative() }) });
59605
+ var RemoteCustomButtonSchema = object({
59606
+ /** The slug after `custom:` in the role. */
59607
+ key: string().refine(isRemoteCustomKey, { message: "a lowercase slug" }),
59608
+ /** What the operator calls it, and what a voice command may name. */
59609
+ name: string().min(1).max(60)
59610
+ });
59611
+ var RemotePowerModeSchema = _enum(["toggle", "discrete"]);
59612
+ var RemoteConfigSchema = object({
59613
+ layoutTemplateId: string().min(1).nullable(),
59614
+ /**
59615
+ * The transmitter device new bindings and an import send through. A binding
59616
+ * names its own device, so one command may override it.
59617
+ */
59618
+ defaultTransmitterId: number().int().nonnegative().nullable().default(null),
59619
+ /** Keyed by role. A role with no entry falls back to the native cap. */
59620
+ bindings: record(string(), RemoteBindingSchema),
59621
+ customButtons: array(RemoteCustomButtonSchema).max(100),
59622
+ power: object({ mode: RemotePowerModeSchema })
59623
+ }).superRefine((config, ctx) => {
59624
+ const roles = Object.keys(config.bindings);
59625
+ if (roles.length > 200) ctx.addIssue({
59626
+ code: "custom",
59627
+ message: "too many bindings",
59628
+ path: ["bindings"]
59629
+ });
59630
+ for (const role of roles) if (!isRemoteRole(role)) ctx.addIssue({
59631
+ code: "custom",
59632
+ message: `"${role}" is not a role`,
59633
+ path: ["bindings", role]
59634
+ });
59635
+ const keys = /* @__PURE__ */ new Set();
59636
+ config.customButtons.forEach((button, index) => {
59637
+ if (keys.has(button.key)) ctx.addIssue({
59638
+ code: "custom",
59639
+ message: `custom button "${button.key}" is listed twice`,
59640
+ path: [
59641
+ "customButtons",
59642
+ index,
59643
+ "key"
59644
+ ]
59645
+ });
59646
+ keys.add(button.key);
59647
+ });
59648
+ for (const role of roles) {
59649
+ const key = customKeyOfRole(role);
59650
+ if (key !== null && !keys.has(key)) ctx.addIssue({
59651
+ code: "custom",
59652
+ message: `${role} is bound but has no entry in customButtons`,
59653
+ path: ["bindings", role]
59654
+ });
59655
+ }
59656
+ });
59657
+ object({
59658
+ power: object({
59659
+ state: _enum(["on", "off"]),
59660
+ at: number()
59661
+ }).nullable(),
59662
+ muted: object({
59663
+ value: boolean(),
59664
+ at: number()
59665
+ }).nullable()
59666
+ });
59667
+ var RemoteLayoutButtonSchema = object({
59668
+ id: string().min(1).max(40),
59669
+ /** 0-based grid position. */
59670
+ row: number().int().min(0).max(24),
59671
+ col: number().int().min(0).max(24),
59672
+ rowSpan: number().int().min(1).max(24),
59673
+ colSpan: number().int().min(1).max(24),
59674
+ icon: string().min(1).max(60).optional(),
59675
+ label: string().min(1).max(40).optional(),
59676
+ role: RemoteRoleSchema
59677
+ });
59678
+ object({
59679
+ columns: number().int().min(1).max(24),
59680
+ rows: number().int().min(1).max(24),
59681
+ buttons: array(RemoteLayoutButtonSchema).max(200)
59682
+ }).superRefine((grid, ctx) => {
59683
+ const ids = /* @__PURE__ */ new Set();
59684
+ const taken = /* @__PURE__ */ new Map();
59685
+ grid.buttons.forEach((button, index) => {
59686
+ if (ids.has(button.id)) ctx.addIssue({
59687
+ code: "custom",
59688
+ message: `button id "${button.id}" is used twice`,
59689
+ path: [
59690
+ "buttons",
59691
+ index,
59692
+ "id"
59693
+ ]
59694
+ });
59695
+ ids.add(button.id);
59696
+ if (button.row + button.rowSpan > grid.rows || button.col + button.colSpan > grid.columns) {
59697
+ ctx.addIssue({
59698
+ code: "custom",
59699
+ message: `button "${button.id}" leaves the grid`,
59700
+ path: ["buttons", index]
59701
+ });
59702
+ return;
59703
+ }
59704
+ for (let r = button.row; r < button.row + button.rowSpan; r += 1) for (let c = button.col; c < button.col + button.colSpan; c += 1) {
59705
+ const cell = `${r}:${c}`;
59706
+ const other = taken.get(cell);
59707
+ if (other !== void 0) {
59708
+ ctx.addIssue({
59709
+ code: "custom",
59710
+ message: `buttons "${other}" and "${button.id}" overlap`,
59711
+ path: ["buttons", index]
59712
+ });
59713
+ return;
59714
+ }
59715
+ taken.set(cell, button.id);
59716
+ }
59717
+ });
59718
+ });
59719
+ var RemoteBoundCommandSchema = object({
59720
+ role: RemoteRoleSchema,
59721
+ /** The operator's name for a custom button; absent for a known role. */
59722
+ label: string().optional(),
59723
+ custom: boolean()
59724
+ });
59725
+ var RemoteCommandListSchema = object({
59726
+ layoutTemplateId: string().nullable(),
59727
+ power: object({ mode: RemotePowerModeSchema }),
59728
+ /** Only the roles that are BOUND — the ones a press can reach. */
59729
+ commands: array(RemoteBoundCommandSchema)
59730
+ });
59731
+ var RemoteRefusalReasonSchema = _enum([
59732
+ "no-remote",
59733
+ "unbound-role",
59734
+ "action-failed"
59735
+ ]);
59736
+ var RemoteSendResultSchema = discriminatedUnion("status", [
59737
+ object({
59738
+ status: literal("sent"),
59739
+ /** How many times the action ran. */
59740
+ sends: number().int().min(1)
59741
+ }),
59742
+ object({
59743
+ status: literal("skipped"),
59744
+ reason: literal("already-in-state")
59745
+ }),
59746
+ object({
59747
+ status: literal("refused"),
59748
+ reason: RemoteRefusalReasonSchema,
59749
+ detail: string().optional()
59750
+ })
59751
+ ]);
59752
+ var RemoteMediaCommandSchema = _enum([
59753
+ "play",
59754
+ "pause",
59755
+ "stop",
59756
+ "next",
59757
+ "previous",
59758
+ "setMute",
59759
+ "selectSource"
59760
+ ]);
59761
+ /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
59762
+ var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
59763
+ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status: literal("saved") }), object({
59764
+ status: literal("refused"),
59765
+ reason: _enum(["unknown-template", "not-a-media-player"]),
59766
+ detail: string()
59767
+ })]);
59768
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
59769
+ DeviceType.MediaPlayer;
59770
+ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
59771
+ kind: "mutation",
59772
+ auth: "admin"
59773
+ }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
59774
+ role: RemoteRoleSchema,
59775
+ /** Sends in a row (volume steps). */
59776
+ repeat: number().int().min(1).max(50).default(1)
59777
+ }), RemoteSendResultSchema, {
59778
+ kind: "mutation",
59779
+ auth: "protected"
59780
+ }), method(deviceInput.extend({
59781
+ command: RemoteMediaCommandSchema,
59782
+ muted: boolean().optional(),
59783
+ source: string().optional(),
59784
+ repeat: number().int().min(1).max(50).default(1)
59785
+ }), RemoteMediaCommandResultSchema, {
59786
+ kind: "mutation",
59787
+ auth: "protected"
59788
+ });
59789
+ var TemplateKindSchema = _enum(["remote"]);
59790
+ /**
59791
+ * template — the generic store of reusable, named operator data (D963). A
59792
+ * record is `{ id, kind, name, payload }`; the kinds (today only `remote`, the
59793
+ * button grid of a remote) and the schema each payload must satisfy are
59794
+ * declared by the system in `template-kinds.ts`. Nothing here is specific to a
59795
+ * kind: the engine validates a payload against its kind's schema and, on a
59796
+ * delete, asks the kind's owner whether anything still points at the record.
59797
+ *
59798
+ * Every refusal is a typed result — a UI says "unknown kind" or names the
59799
+ * devices that still use a template, without parsing text.
59800
+ */
59801
+ var TemplateRecordSchema = object({
59802
+ id: string().min(1),
59803
+ kind: TemplateKindSchema,
59804
+ name: string().min(1).max(120),
59805
+ /** Shaped by the kind; `checkTemplatePayload(kind, payload)` reads it. */
59806
+ payload: unknown(),
59807
+ createdAt: number(),
59808
+ updatedAt: number()
59809
+ });
59810
+ var TemplateWriteResultSchema = discriminatedUnion("status", [object({
59811
+ status: literal("saved"),
59812
+ template: TemplateRecordSchema
59813
+ }), object({
59814
+ status: literal("refused"),
59815
+ reason: _enum(["invalid-payload", "not-found"]),
59816
+ detail: string()
59817
+ })]);
59818
+ /** A device still pointing at a template. */
59819
+ var TemplateUserSchema = object({
59820
+ deviceId: number().int().nonnegative(),
59821
+ name: string()
59822
+ });
59823
+ var TemplateDeleteResultSchema = discriminatedUnion("status", [
59824
+ object({ status: literal("deleted") }),
59825
+ object({ status: literal("not-found") }),
59826
+ object({
59827
+ status: literal("in-use"),
59828
+ devices: array(TemplateUserSchema).min(1)
59829
+ })
59830
+ ]);
59831
+ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSchema), { auth: "protected" }), method(object({ id: string().min(1) }), TemplateRecordSchema.nullable(), { auth: "protected" }), method(object({
59832
+ kind: TemplateKindSchema,
59833
+ name: string().min(1).max(120),
59834
+ payload: unknown()
59835
+ }), TemplateWriteResultSchema, {
59836
+ kind: "mutation",
59837
+ auth: "admin"
59838
+ }), method(object({
59839
+ id: string().min(1),
59840
+ name: string().min(1).max(120).optional(),
59841
+ payload: unknown().optional()
59842
+ }), TemplateWriteResultSchema, {
59843
+ kind: "mutation",
59844
+ auth: "admin"
59845
+ }), method(object({ id: string().min(1) }), TemplateDeleteResultSchema, {
59846
+ kind: "mutation",
59847
+ auth: "admin"
59848
+ });
59849
+ /**
59216
59850
  * button — device-scoped capability for HA `button.*` / `input_button.*`
59217
59851
  * fire-only actuators. A button entity has no readable state — pressing it
59218
59852
  * triggers an action on the HA side. There is no `status` / `runtimeState`:
@@ -60267,14 +60901,13 @@ reset: method(object({
60267
60901
  auth: "admin"
60268
60902
  }) },
60269
60903
  /**
60270
- * A composed consumable (item added to a device with no native consumables
60271
- * provider) refuses `reset` by name: "Only meaningful when the item's
60272
- * `resettable` is true" — a composed item is never resettable until
60273
- * commands (`forward`) arrive in slice 3 (D663).
60904
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
60905
+ * name: only a composed device of a NEW type forwards an item's reset to a
60906
+ * source button, and marks that item `resettable` (D967).
60274
60907
  */
60275
60908
  composedMethods: { reset: {
60276
60909
  kind: "refuse",
60277
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
60910
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
60278
60911
  } },
60279
60912
  status: {
60280
60913
  schema: ConsumablesStatusSchema,
@@ -60868,7 +61501,7 @@ var DishwasherStatusSchema = object({
60868
61501
  /** Ms epoch of the last value received from the appliance; 0 = never. */
60869
61502
  lastFetchedAt: number()
60870
61503
  });
60871
- var DeviceIdSchema = number().int().nonnegative();
61504
+ var DeviceIdSchema$1 = number().int().nonnegative();
60872
61505
  /** A value a program option or setting can take. */
60873
61506
  var DishwasherValueSchema = union([
60874
61507
  boolean(),
@@ -60943,77 +61576,77 @@ var dishwasherCapability = {
60943
61576
  * option feature keys; each is checked against the program's constraints.
60944
61577
  */
60945
61578
  start: method(object({
60946
- deviceId: DeviceIdSchema,
61579
+ deviceId: DeviceIdSchema$1,
60947
61580
  program: string().min(1).optional(),
60948
61581
  options: record(string(), DishwasherValueSchema).optional()
60949
61582
  }), _void(), {
60950
61583
  kind: "mutation",
60951
61584
  auth: "protected"
60952
61585
  }),
60953
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
61586
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60954
61587
  kind: "mutation",
60955
61588
  auth: "protected"
60956
61589
  }),
60957
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
61590
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60958
61591
  kind: "mutation",
60959
61592
  auth: "protected"
60960
61593
  }),
60961
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
61594
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60962
61595
  kind: "mutation",
60963
61596
  auth: "protected"
60964
61597
  }),
60965
61598
  /** Select (not start) a program; refused when the appliance does not offer it. */
60966
61599
  setProgram: method(object({
60967
- deviceId: DeviceIdSchema,
61600
+ deviceId: DeviceIdSchema$1,
60968
61601
  program: string().min(1)
60969
61602
  }), _void(), {
60970
61603
  kind: "mutation",
60971
61604
  auth: "protected"
60972
61605
  }),
60973
61606
  setPower: method(object({
60974
- deviceId: DeviceIdSchema,
61607
+ deviceId: DeviceIdSchema$1,
60975
61608
  on: boolean()
60976
61609
  }), _void(), {
60977
61610
  kind: "mutation",
60978
61611
  auth: "protected"
60979
61612
  }),
60980
61613
  setHalfLoad: method(object({
60981
- deviceId: DeviceIdSchema,
61614
+ deviceId: DeviceIdSchema$1,
60982
61615
  on: boolean()
60983
61616
  }), _void(), {
60984
61617
  kind: "mutation",
60985
61618
  auth: "protected"
60986
61619
  }),
60987
61620
  setHygienePlus: method(object({
60988
- deviceId: DeviceIdSchema,
61621
+ deviceId: DeviceIdSchema$1,
60989
61622
  on: boolean()
60990
61623
  }), _void(), {
60991
61624
  kind: "mutation",
60992
61625
  auth: "protected"
60993
61626
  }),
60994
61627
  setExtraDry: method(object({
60995
- deviceId: DeviceIdSchema,
61628
+ deviceId: DeviceIdSchema$1,
60996
61629
  on: boolean()
60997
61630
  }), _void(), {
60998
61631
  kind: "mutation",
60999
61632
  auth: "protected"
61000
61633
  }),
61001
61634
  setIntensiveZone: method(object({
61002
- deviceId: DeviceIdSchema,
61635
+ deviceId: DeviceIdSchema$1,
61003
61636
  on: boolean()
61004
61637
  }), _void(), {
61005
61638
  kind: "mutation",
61006
61639
  auth: "protected"
61007
61640
  }),
61008
61641
  setVarioSpeedPlus: method(object({
61009
- deviceId: DeviceIdSchema,
61642
+ deviceId: DeviceIdSchema$1,
61010
61643
  on: boolean()
61011
61644
  }), _void(), {
61012
61645
  kind: "mutation",
61013
61646
  auth: "protected"
61014
61647
  }),
61015
61648
  setChildLock: method(object({
61016
- deviceId: DeviceIdSchema,
61649
+ deviceId: DeviceIdSchema$1,
61017
61650
  on: boolean()
61018
61651
  }), _void(), {
61019
61652
  kind: "mutation",
@@ -61025,7 +61658,7 @@ var dishwasherCapability = {
61025
61658
  * its declared range / members.
61026
61659
  */
61027
61660
  setOption: method(object({
61028
- deviceId: DeviceIdSchema,
61661
+ deviceId: DeviceIdSchema$1,
61029
61662
  key: string().min(1),
61030
61663
  value: DishwasherValueSchema
61031
61664
  }), _void(), {
@@ -61033,7 +61666,7 @@ var dishwasherCapability = {
61033
61666
  auth: "protected"
61034
61667
  }),
61035
61668
  /** What this appliance can do, from its own description — the UI is derived from it. */
61036
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
61669
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
61037
61670
  },
61038
61671
  status: {
61039
61672
  schema: DishwasherStatusSchema,
@@ -63414,9 +64047,35 @@ var MediaInfoSchema = object({
63414
64047
  /** Optional cover-art / thumbnail URL. */
63415
64048
  imageUrl: string().optional()
63416
64049
  });
64050
+ /**
64051
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
64052
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
64053
+ * tracks what it last SENT; it moves only after a send was accepted, and a
64054
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
64055
+ * guess, never as a reading.
64056
+ */
64057
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
64058
+ /** The fields of a media-player status a driver can only guess. */
64059
+ var MediaPlayerAssumedFieldSchema = _enum([
64060
+ "state",
64061
+ "volumeLevel",
64062
+ "isMuted",
64063
+ "source"
64064
+ ]);
63417
64065
  var MediaPlayerStatusSchema = object({
63418
64066
  /** Playback lifecycle state. */
63419
64067
  state: MediaPlayerStateSchema,
64068
+ /**
64069
+ * Whether the state fields are readings or the driver's own bookkeeping.
64070
+ * A slice written before this field existed parses as `reported`.
64071
+ */
64072
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
64073
+ /**
64074
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
64075
+ * this is not empty; a field not listed here is a reading even then (power
64076
+ * from a running-state source, mute guessed from the last send).
64077
+ */
64078
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
63420
64079
  /** Volume as 0..100 inclusive. Null when the device has no volume
63421
64080
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
63422
64081
  volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
@@ -64708,7 +65367,8 @@ var TopologyNodeSchema = object({
64708
65367
  isHub: boolean(),
64709
65368
  isOnline: boolean(),
64710
65369
  cpuPercent: number(),
64711
- memoryPercent: number(),
65370
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
65371
+ memoryPercent: number().nullable(),
64712
65372
  uptime: number(),
64713
65373
  lastSeen: string(),
64714
65374
  localIps: array(string()).readonly(),
@@ -65326,9 +65986,10 @@ var PetFeederStatusSchema = object({
65326
65986
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
65327
65987
  * the device has not reported yet. */
65328
65988
  foodLevel: number().nullable().meta(readingMeta("none")),
65329
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
65330
- * feeder's own units. Null when the model reports none. */
65331
- bowlFood: number().nullable().meta(readingMeta("none")),
65989
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
65990
+ * feeder with a bowl scale; null when the model has none or its reading is
65991
+ * of an unproven unit. */
65992
+ bowlFood: number().nullable().meta(readingMeta("mass")),
65332
65993
  /** True while the pet is eating (`state.eating`). */
65333
65994
  eating: boolean(),
65334
65995
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -65379,6 +66040,14 @@ var PetFeederStatusSchema = object({
65379
66040
  feedSound: boolean(),
65380
66041
  /** Speaker / prompt volume level (device-scaled integer). */
65381
66042
  volume: number().meta(readingMeta("none")),
66043
+ /** Portion (grams) of the last manual feed sent from a single-hopper
66044
+ * stepper — the default the next manual feed starts from. Persisted per
66045
+ * device by the provider; null until a manual feed has been sent. */
66046
+ lastFeedGrams: gramsPortion.nullable(),
66047
+ /** Hopper-1 portion of the last manual feed (dual-hopper); null until sent. */
66048
+ lastFeedHopper1: gramsPortion.nullable(),
66049
+ /** Hopper-2 portion of the last manual feed (dual-hopper); null until sent. */
66050
+ lastFeedHopper2: gramsPortion.nullable(),
65382
66051
  /** Ms epoch when the slice was last refreshed from the cloud. */
65383
66052
  lastFetchedAt: number()
65384
66053
  });
@@ -65504,6 +66173,137 @@ var petFeederCapability = {
65504
66173
  }
65505
66174
  } }
65506
66175
  };
66176
+ /**
66177
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
66178
+ * circulating drinking fountain: how much water is in the tank, how much the
66179
+ * pet drank, whether the pump runs and on what duty cycle.
66180
+ *
66181
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
66182
+ * a container of loose entities (a pump switch, two number boxes, a "filter
66183
+ * changed" button), and an operator BUILDS the device from them — a composed
66184
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
66185
+ * method of ONE source entity, and every reading is a field derived from a
66186
+ * source entity; the provider-shaped interface below exists so the composed
66187
+ * device and any future native provider answer the same contract.
66188
+ *
66189
+ * Quantities are stored in the base unit of their kind (D944): volumes in
66190
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
66191
+ * reports millilitres or minutes is converted in the composition's expression,
66192
+ * never here. Every reading may be `null`: a source that is offline or does
66193
+ * not report is unknown, never zero.
66194
+ *
66195
+ * The filter is not a field of this cap: it is an item of the `consumables`
66196
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
66197
+ */
66198
+ var PetFountainStatusSchema = object({
66199
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
66200
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66201
+ /** Water in the tank, in litres. Null while the source has not reported. */
66202
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
66203
+ /** True when the tank is empty. Null while unknown. */
66204
+ empty: boolean().nullable(),
66205
+ /** True while the pump is running. Null while unknown. */
66206
+ pumpRunning: boolean().nullable(),
66207
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
66208
+ pumpIntervalEnabled: boolean().nullable(),
66209
+ /** How long the pump runs per cycle, in seconds. */
66210
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
66211
+ /** How long the pump rests per cycle, in seconds. */
66212
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
66213
+ /** Water drunk in the last drink, in litres. */
66214
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
66215
+ /** Water drunk in the current hour, in litres. */
66216
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
66217
+ /** Water drunk over the last 24 hours (rolling), in litres. */
66218
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
66219
+ /** Water drunk since local midnight, in litres. */
66220
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
66221
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
66222
+ error: string().nullable(),
66223
+ /** Ms epoch when the slice was last refreshed. */
66224
+ lastFetchedAt: number()
66225
+ });
66226
+ var DeviceIdSchema = number().int().nonnegative();
66227
+ var MinutesSchema = number().int().min(0);
66228
+ var petFountainCapability = {
66229
+ name: "pet-fountain",
66230
+ scope: "device",
66231
+ deviceNative: true,
66232
+ mode: "singleton",
66233
+ deviceTypes: [DeviceType.PetFountain],
66234
+ methods: {
66235
+ /** Start or stop the pump. */
66236
+ setPump: method(object({
66237
+ deviceId: DeviceIdSchema,
66238
+ on: boolean()
66239
+ }), _void(), {
66240
+ kind: "mutation",
66241
+ auth: "protected"
66242
+ }),
66243
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
66244
+ setPumpInterval: method(object({
66245
+ deviceId: DeviceIdSchema,
66246
+ on: boolean()
66247
+ }), _void(), {
66248
+ kind: "mutation",
66249
+ auth: "protected"
66250
+ }),
66251
+ /** Set how many minutes the pump runs per cycle. */
66252
+ setPumpOnMinutes: method(object({
66253
+ deviceId: DeviceIdSchema,
66254
+ minutes: MinutesSchema
66255
+ }), _void(), {
66256
+ kind: "mutation",
66257
+ auth: "protected"
66258
+ }),
66259
+ /** Set how many minutes the pump rests per cycle. */
66260
+ setPumpOffMinutes: method(object({
66261
+ deviceId: DeviceIdSchema,
66262
+ minutes: MinutesSchema
66263
+ }), _void(), {
66264
+ kind: "mutation",
66265
+ auth: "protected"
66266
+ }),
66267
+ /** Clear the rolling daily consumption counters on the device. */
66268
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
66269
+ kind: "mutation",
66270
+ auth: "protected"
66271
+ })
66272
+ },
66273
+ status: {
66274
+ schema: PetFountainStatusSchema,
66275
+ kind: "push"
66276
+ },
66277
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
66278
+ runtimeState: PetFountainStatusSchema,
66279
+ /**
66280
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
66281
+ *
66282
+ * See `RuntimeStateDurability`. Enforced by
66283
+ * `scripts/check-runtime-state-durability.ts`.
66284
+ */
66285
+ durability: "session",
66286
+ /** History (D850): recorded series. */
66287
+ history: { fields: {
66288
+ waterLevel: {
66289
+ kind: "gauge",
66290
+ unit: "%",
66291
+ deadband: 1
66292
+ },
66293
+ waterVolume: {
66294
+ kind: "gauge",
66295
+ unit: "L"
66296
+ },
66297
+ todayDrink: {
66298
+ kind: "gauge",
66299
+ unit: "L"
66300
+ },
66301
+ last24hDrink: {
66302
+ kind: "gauge",
66303
+ unit: "L"
66304
+ }
66305
+ } }
66306
+ };
65507
66307
  var VehicleSchema = object({
65508
66308
  id: string(),
65509
66309
  name: string(),
@@ -71424,6 +72224,14 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
71424
72224
  kind: "mutation",
71425
72225
  auth: "admin"
71426
72226
  });
72227
+ discriminatedUnion("kind", [object({
72228
+ kind: literal("raw"),
72229
+ timings: array(number().int()),
72230
+ settings: TransmitSettingsSchema.optional()
72231
+ }), object({
72232
+ kind: literal("pronto"),
72233
+ code: string()
72234
+ })]);
71427
72235
  function resolveSince(running, previous, transitionAtMs, bootstrapSinceMs) {
71428
72236
  if (running === null) return null;
71429
72237
  const prevRunning = previous?.running ?? null;
@@ -71861,7 +72669,7 @@ function tryConvertDelta(value, from, to) {
71861
72669
  return shifted - origin;
71862
72670
  }
71863
72671
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
71864
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
72672
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
71865
72673
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
71866
72674
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
71867
72675
  new Set(CAP_NAMES);
@@ -72212,6 +73020,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
72212
73020
  notifier: notifierCapability,
72213
73021
  numericSensor: numericSensorCapability,
72214
73022
  petFeeder: petFeederCapability,
73023
+ petFountain: petFountainCapability,
72215
73024
  powerMeter: powerMeterCapability,
72216
73025
  presence: presenceCapability,
72217
73026
  pressureSensor: pressureSensorCapability,
@@ -73462,7 +74271,7 @@ var BaseDeviceProvider = class extends BaseAddon {
73462
74271
  return toDeviceSummary(device, this.addonId);
73463
74272
  }
73464
74273
  };
73465
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
74274
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
73466
74275
  new Set(Object.values(DeviceType));
73467
74276
  Object.freeze({
73468
74277
  "accessories.getStatus": {
@@ -75397,6 +76206,13 @@ Object.freeze({
75397
76206
  access: "create",
75398
76207
  role: "admin"
75399
76208
  },
76209
+ "deviceExport.exposeEntity": {
76210
+ capName: "device-export",
76211
+ capScope: "system",
76212
+ addonId: null,
76213
+ access: "create",
76214
+ role: "admin"
76215
+ },
75400
76216
  "deviceExport.getDeviceLiveContribution": {
75401
76217
  capName: "device-export",
75402
76218
  capScope: "system",
@@ -75425,6 +76241,13 @@ Object.freeze({
75425
76241
  access: "view",
75426
76242
  role: "admin"
75427
76243
  },
76244
+ "deviceExport.listExposedEntities": {
76245
+ capName: "device-export",
76246
+ capScope: "system",
76247
+ addonId: null,
76248
+ access: "view",
76249
+ role: "admin"
76250
+ },
75428
76251
  "deviceExport.listSupportedDeviceKinds": {
75429
76252
  capName: "device-export",
75430
76253
  capScope: "system",
@@ -75432,6 +76255,13 @@ Object.freeze({
75432
76255
  access: "view",
75433
76256
  role: "admin"
75434
76257
  },
76258
+ "deviceExport.listSupportedEntityKinds": {
76259
+ capName: "device-export",
76260
+ capScope: "system",
76261
+ addonId: null,
76262
+ access: "view",
76263
+ role: "admin"
76264
+ },
75435
76265
  "deviceExport.unexposeDevice": {
75436
76266
  capName: "device-export",
75437
76267
  capScope: "system",
@@ -75439,6 +76269,13 @@ Object.freeze({
75439
76269
  access: "create",
75440
76270
  role: "admin"
75441
76271
  },
76272
+ "deviceExport.unexposeEntity": {
76273
+ capName: "device-export",
76274
+ capScope: "system",
76275
+ addonId: null,
76276
+ access: "create",
76277
+ role: "admin"
76278
+ },
75442
76279
  "deviceExtension.listCandidates": {
75443
76280
  capName: "device-extension",
75444
76281
  capScope: "device",
@@ -78260,6 +79097,13 @@ Object.freeze({
78260
79097
  access: "create",
78261
79098
  role: "admin"
78262
79099
  },
79100
+ "nodeRemovalHook.onNodeRemoved": {
79101
+ capName: "node-removal-hook",
79102
+ capScope: "system",
79103
+ addonId: null,
79104
+ access: "create",
79105
+ role: "admin"
79106
+ },
78263
79107
  "nodes.clusterAddonStatus": {
78264
79108
  capName: "nodes",
78265
79109
  capScope: "system",
@@ -78897,6 +79741,48 @@ Object.freeze({
78897
79741
  access: "create",
78898
79742
  role: "viewer"
78899
79743
  },
79744
+ "petFountain.getStatus": {
79745
+ capName: "pet-fountain",
79746
+ capScope: "device",
79747
+ addonId: null,
79748
+ access: "view",
79749
+ role: "viewer"
79750
+ },
79751
+ "petFountain.resetDailyConsumption": {
79752
+ capName: "pet-fountain",
79753
+ capScope: "device",
79754
+ addonId: null,
79755
+ access: "delete",
79756
+ role: "viewer"
79757
+ },
79758
+ "petFountain.setPump": {
79759
+ capName: "pet-fountain",
79760
+ capScope: "device",
79761
+ addonId: null,
79762
+ access: "create",
79763
+ role: "viewer"
79764
+ },
79765
+ "petFountain.setPumpInterval": {
79766
+ capName: "pet-fountain",
79767
+ capScope: "device",
79768
+ addonId: null,
79769
+ access: "create",
79770
+ role: "viewer"
79771
+ },
79772
+ "petFountain.setPumpOffMinutes": {
79773
+ capName: "pet-fountain",
79774
+ capScope: "device",
79775
+ addonId: null,
79776
+ access: "create",
79777
+ role: "viewer"
79778
+ },
79779
+ "petFountain.setPumpOnMinutes": {
79780
+ capName: "pet-fountain",
79781
+ capScope: "device",
79782
+ addonId: null,
79783
+ access: "create",
79784
+ role: "viewer"
79785
+ },
78900
79786
  "pipelineAnalytics.addRetrainFrameFromRecording": {
78901
79787
  capName: "pipeline-analytics",
78902
79788
  capScope: "device",
@@ -81151,6 +82037,41 @@ Object.freeze({
81151
82037
  access: "create",
81152
82038
  role: "admin"
81153
82039
  },
82040
+ "remote.getConfig": {
82041
+ capName: "remote",
82042
+ capScope: "system",
82043
+ addonId: null,
82044
+ access: "view",
82045
+ role: "admin"
82046
+ },
82047
+ "remote.listCommands": {
82048
+ capName: "remote",
82049
+ capScope: "system",
82050
+ addonId: null,
82051
+ access: "view",
82052
+ role: "admin"
82053
+ },
82054
+ "remote.sendCommand": {
82055
+ capName: "remote",
82056
+ capScope: "system",
82057
+ addonId: null,
82058
+ access: "create",
82059
+ role: "admin"
82060
+ },
82061
+ "remote.sendMediaCommand": {
82062
+ capName: "remote",
82063
+ capScope: "system",
82064
+ addonId: null,
82065
+ access: "create",
82066
+ role: "admin"
82067
+ },
82068
+ "remote.setConfig": {
82069
+ capName: "remote",
82070
+ capScope: "system",
82071
+ addonId: null,
82072
+ access: "create",
82073
+ role: "admin"
82074
+ },
81154
82075
  "runningState.getStatus": {
81155
82076
  capName: "running-state",
81156
82077
  capScope: "device",
@@ -82341,6 +83262,41 @@ Object.freeze({
82341
83262
  access: "view",
82342
83263
  role: "viewer"
82343
83264
  },
83265
+ "template.create": {
83266
+ capName: "template",
83267
+ capScope: "system",
83268
+ addonId: null,
83269
+ access: "create",
83270
+ role: "admin"
83271
+ },
83272
+ "template.delete": {
83273
+ capName: "template",
83274
+ capScope: "system",
83275
+ addonId: null,
83276
+ access: "delete",
83277
+ role: "admin"
83278
+ },
83279
+ "template.get": {
83280
+ capName: "template",
83281
+ capScope: "system",
83282
+ addonId: null,
83283
+ access: "view",
83284
+ role: "admin"
83285
+ },
83286
+ "template.list": {
83287
+ capName: "template",
83288
+ capScope: "system",
83289
+ addonId: null,
83290
+ access: "view",
83291
+ role: "admin"
83292
+ },
83293
+ "template.update": {
83294
+ capName: "template",
83295
+ capScope: "system",
83296
+ addonId: null,
83297
+ access: "create",
83298
+ role: "admin"
83299
+ },
82344
83300
  "terminalSession.adoptLegacyMonitor": {
82345
83301
  capName: "terminal-session",
82346
83302
  capScope: "system",
@@ -85309,6 +86265,100 @@ var APPLIANCE_CAP_VOCABULARY = {
85309
86265
  }
85310
86266
  ]
85311
86267
  },
86268
+ "pet-fountain": {
86269
+ scope: "device",
86270
+ noun: t("Pet fountain", "Fontana per animali"),
86271
+ actions: [
86272
+ {
86273
+ id: "petFountain.pumpOn",
86274
+ method: "setPump",
86275
+ label: t("Pump on", "Pompa accesa"),
86276
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
86277
+ icon: "play",
86278
+ args: { on: true }
86279
+ },
86280
+ {
86281
+ id: "petFountain.pumpOff",
86282
+ method: "setPump",
86283
+ label: t("Pump off", "Pompa spenta"),
86284
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
86285
+ icon: "square",
86286
+ args: { on: false }
86287
+ },
86288
+ {
86289
+ id: "petFountain.intervalOn",
86290
+ method: "setPumpInterval",
86291
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
86292
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
86293
+ icon: "timer",
86294
+ args: { on: true }
86295
+ },
86296
+ {
86297
+ id: "petFountain.intervalOff",
86298
+ method: "setPumpInterval",
86299
+ label: t("Pump cycle off", "Ciclo pompa spento"),
86300
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
86301
+ icon: "timer-off",
86302
+ args: { on: false }
86303
+ },
86304
+ {
86305
+ id: "petFountain.pumpOnMinutes",
86306
+ method: "setPumpOnMinutes",
86307
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
86308
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
86309
+ icon: "timer",
86310
+ openArgs: [{
86311
+ key: "minutes",
86312
+ label: t("Minutes", "Minuti"),
86313
+ offer: {
86314
+ kind: "number",
86315
+ min: 0,
86316
+ step: 1,
86317
+ unit: "min"
86318
+ }
86319
+ }]
86320
+ },
86321
+ {
86322
+ id: "petFountain.pumpOffMinutes",
86323
+ method: "setPumpOffMinutes",
86324
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
86325
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
86326
+ icon: "timer-off",
86327
+ openArgs: [{
86328
+ key: "minutes",
86329
+ label: t("Minutes", "Minuti"),
86330
+ offer: {
86331
+ kind: "number",
86332
+ min: 0,
86333
+ step: 1,
86334
+ unit: "min"
86335
+ }
86336
+ }]
86337
+ },
86338
+ {
86339
+ id: "petFountain.resetDailyConsumption",
86340
+ method: "resetDailyConsumption",
86341
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
86342
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
86343
+ icon: "rotate-ccw"
86344
+ }
86345
+ ],
86346
+ state: [{
86347
+ kind: "boolean",
86348
+ field: "empty",
86349
+ label: t("Tank empty", "Serbatoio vuoto"),
86350
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
86351
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
86352
+ icon: "droplet"
86353
+ }, {
86354
+ kind: "numeric",
86355
+ field: "waterLevel",
86356
+ quantity: t("the water level", "il livello dell’acqua"),
86357
+ label: t("Water level", "Livello dell’acqua"),
86358
+ unit: "%",
86359
+ icon: "droplets"
86360
+ }]
86361
+ },
85312
86362
  "media-player": {
85313
86363
  scope: "device",
85314
86364
  noun: t("Media player", "Lettore multimediale"),
@@ -87133,6 +88183,7 @@ var SYSTEM_CAP_VOCABULARY = {
87133
88183
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
87134
88184
  "device-provider": system(INTERNAL),
87135
88185
  "device-removal-hook": system(INTERNAL),
88186
+ "node-removal-hook": system(INTERNAL),
87136
88187
  "device-state": system("the runtime-state mirror every trigger already reads through"),
87137
88188
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
87138
88189
  "embedding-encoder": system(ML),
@@ -87172,6 +88223,7 @@ var SYSTEM_CAP_VOCABULARY = {
87172
88223
  "recording-archive": system(STORAGE),
87173
88224
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
87174
88225
  records: system(INTERNAL),
88226
+ remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
87175
88227
  "server-management": system(CLUSTER),
87176
88228
  "settings-store": system(INTERNAL),
87177
88229
  "smtp-provider": system(INTERNAL),
@@ -87184,6 +88236,7 @@ var SYSTEM_CAP_VOCABULARY = {
87184
88236
  "storage-provider": system(STORAGE),
87185
88237
  "stream-broker": system(PIPELINE),
87186
88238
  system: system(CLUSTER),
88239
+ template: system("operator-authored templates — edited in their own editor, never actuated"),
87187
88240
  "terminal-session": system(CLUSTER),
87188
88241
  toast: system(UI_PLUMBING),
87189
88242
  training: system(ML),
@@ -87243,7 +88296,7 @@ function projectAction(cap, action) {
87243
88296
  };
87244
88297
  }
87245
88298
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
87246
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
88299
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
87247
88300
  DeviceFeature.BatteryOperated;
87248
88301
  /**
87249
88302
  * How a washer, dryer or dishwasher leaves CamStack.
@@ -88805,6 +89858,36 @@ Object.freeze({
88805
89858
  form: "single",
88806
89859
  optional: false
88807
89860
  }],
89861
+ "petFountain.getStatus": [{
89862
+ name: "deviceId",
89863
+ form: "single",
89864
+ optional: false
89865
+ }],
89866
+ "petFountain.resetDailyConsumption": [{
89867
+ name: "deviceId",
89868
+ form: "single",
89869
+ optional: false
89870
+ }],
89871
+ "petFountain.setPump": [{
89872
+ name: "deviceId",
89873
+ form: "single",
89874
+ optional: false
89875
+ }],
89876
+ "petFountain.setPumpInterval": [{
89877
+ name: "deviceId",
89878
+ form: "single",
89879
+ optional: false
89880
+ }],
89881
+ "petFountain.setPumpOffMinutes": [{
89882
+ name: "deviceId",
89883
+ form: "single",
89884
+ optional: false
89885
+ }],
89886
+ "petFountain.setPumpOnMinutes": [{
89887
+ name: "deviceId",
89888
+ form: "single",
89889
+ optional: false
89890
+ }],
88808
89891
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
88809
89892
  name: "deviceId",
88810
89893
  form: "single",
@@ -89621,6 +90704,31 @@ Object.freeze({
89621
90704
  form: "single",
89622
90705
  optional: true
89623
90706
  }],
90707
+ "remote.getConfig": [{
90708
+ name: "deviceId",
90709
+ form: "single",
90710
+ optional: false
90711
+ }],
90712
+ "remote.listCommands": [{
90713
+ name: "deviceId",
90714
+ form: "single",
90715
+ optional: false
90716
+ }],
90717
+ "remote.sendCommand": [{
90718
+ name: "deviceId",
90719
+ form: "single",
90720
+ optional: false
90721
+ }],
90722
+ "remote.sendMediaCommand": [{
90723
+ name: "deviceId",
90724
+ form: "single",
90725
+ optional: false
90726
+ }],
90727
+ "remote.setConfig": [{
90728
+ name: "deviceId",
90729
+ form: "single",
90730
+ optional: false
90731
+ }],
89624
90732
  "runningState.getStatus": [{
89625
90733
  name: "deviceId",
89626
90734
  form: "single",
@@ -91778,14 +92886,6 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
91778
92886
  String.raw`(?<=^|["'(\s>,\[{])`;
91779
92887
  var MB = 1024 * 1024;
91780
92888
  1024 * MB, 3072 * MB;
91781
- discriminatedUnion("kind", [object({
91782
- kind: literal("raw"),
91783
- timings: array(number().int()),
91784
- settings: TransmitSettingsSchema.optional()
91785
- }), object({
91786
- kind: literal("pronto"),
91787
- code: string()
91788
- })]);
91789
92889
  object({
91790
92890
  manufacturer: string().optional(),
91791
92891
  supportedModels: array(string()).optional(),
@@ -92317,6 +93417,12 @@ Object.defineProperty(exports, "pressureSensorCapability", {
92317
93417
  return pressureSensorCapability;
92318
93418
  }
92319
93419
  });
93420
+ Object.defineProperty(exports, "pruneDeletedEntities", {
93421
+ enumerable: true,
93422
+ get: function() {
93423
+ return pruneDeletedEntities;
93424
+ }
93425
+ });
92320
93426
  Object.defineProperty(exports, "quantityFromUnit", {
92321
93427
  enumerable: true,
92322
93428
  get: function() {
@@ -92329,6 +93435,12 @@ Object.defineProperty(exports, "record", {
92329
93435
  return record;
92330
93436
  }
92331
93437
  });
93438
+ Object.defineProperty(exports, "runExportedAction", {
93439
+ enumerable: true,
93440
+ get: function() {
93441
+ return runExportedAction;
93442
+ }
93443
+ });
92332
93444
  Object.defineProperty(exports, "runningFromLawnMower", {
92333
93445
  enumerable: true,
92334
93446
  get: function() {
@@ -92359,6 +93471,12 @@ Object.defineProperty(exports, "scriptRunnerCapability", {
92359
93471
  return scriptRunnerCapability;
92360
93472
  }
92361
93473
  });
93474
+ Object.defineProperty(exports, "sendToExportedTarget", {
93475
+ enumerable: true,
93476
+ get: function() {
93477
+ return sendToExportedTarget;
93478
+ }
93479
+ });
92362
93480
  Object.defineProperty(exports, "smokeCapability", {
92363
93481
  enumerable: true,
92364
93482
  get: function() {