@camstack/addon-import-alexa 0.2.166 → 0.2.168

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.
package/dist/addon.js CHANGED
@@ -5248,6 +5248,14 @@ function object(shape, params) {
5248
5248
  ...normalizeParams(params)
5249
5249
  });
5250
5250
  }
5251
+ function strictObject(shape, params) {
5252
+ return new ZodObject({
5253
+ type: "object",
5254
+ shape,
5255
+ catchall: never(),
5256
+ ...normalizeParams(params)
5257
+ });
5258
+ }
5251
5259
  function looseObject(shape, params) {
5252
5260
  return new ZodObject({
5253
5261
  type: "object",
@@ -5627,7 +5635,7 @@ var ZodIssueCode = {
5627
5635
  var ZodFirstPartyTypeKind;
5628
5636
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5629
5637
  //#endregion
5630
- //#region ../types/dist/sleep-C_u6cEAJ.mjs
5638
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5631
5639
  /**
5632
5640
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5633
5641
  * window to float samples (D455).
@@ -8862,9 +8870,76 @@ function optionalMethod(input, output, options) {
8862
8870
  function event(data) {
8863
8871
  return { data };
8864
8872
  }
8873
+ /**
8874
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8875
+ * declarations. Every addon that contributes a UI page registers a
8876
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8877
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8878
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8879
+ * that admin-ui consumes.
8880
+ *
8881
+ * The split exists because the public listing has a different output
8882
+ * shape than the per-provider raw declarations, and we want both ends
8883
+ * to flow through codegen instead of relying on a hand-written wrapper.
8884
+ */
8885
+ var AddonPageDeclarationSchema = object({
8886
+ id: string(),
8887
+ label: string(),
8888
+ icon: string(),
8889
+ path: string(),
8890
+ /**
8891
+ * Module Federation remote name — must match the `name` field on the
8892
+ * page addon's `federation()` plugin config. Used by admin-ui's
8893
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8894
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8895
+ * contain hyphens).
8896
+ */
8897
+ remoteName: string(),
8898
+ /**
8899
+ * Bundle filename inside the addon's `dist/` dir served at
8900
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8901
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8902
+ * the static-file route can compute an mtime-based cache-buster URL
8903
+ * without a separate filesystem stat.
8904
+ */
8905
+ bundle: string(),
8906
+ /**
8907
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8908
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8909
+ * Any OTHER string creates (or joins) a custom section rendered after
8910
+ * the built-in groups; its label comes from `sectionLabel` (first
8911
+ * declaration wins), falling back to the id. Absent → the legacy
8912
+ * "Addon Pages" group.
8913
+ */
8914
+ section: string().optional(),
8915
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8916
+ sectionLabel: string().optional()
8917
+ });
8918
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8865
8919
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8866
8920
  var VersionOutputSchema$1 = object({ version: string() });
8867
8921
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8922
+ /**
8923
+ * The pipeline step-input shape and the run-result bridge, in a module that
8924
+ * imports nothing but zod and types.
8925
+ *
8926
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8927
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8928
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8929
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8930
+ * them, so every existing import keeps working; addon server code takes them
8931
+ * from `@camstack/types/addon`.
8932
+ */
8933
+ var PipelineStepInputSchema = lazy(() => object({
8934
+ addonId: string(),
8935
+ modelId: string().optional(),
8936
+ enabled: boolean().default(true),
8937
+ children: array(PipelineStepInputSchema).optional(),
8938
+ settings: record(string(), unknown()).optional(),
8939
+ jumpDeviceKey: string().optional()
8940
+ }));
8941
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8942
+ var PipelineRunResultBridge = custom();
8868
8943
  var StaticDirOutputSchema = object({ staticDir: string() });
8869
8944
  var VersionOutputSchema = object({ version: string() });
8870
8945
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9060,6 +9135,7 @@ var CAP_NAMES = Object.freeze([
9060
9135
  "osd",
9061
9136
  "osd-manager",
9062
9137
  "pet-feeder",
9138
+ "pet-fountain",
9063
9139
  "pipeline-analytics",
9064
9140
  "pipeline-executor",
9065
9141
  "pipeline-orchestrator",
@@ -10339,6 +10415,56 @@ Object.freeze({
10339
10415
  "valueType": "number"
10340
10416
  }
10341
10417
  ],
10418
+ "pet-fountain": [
10419
+ {
10420
+ "path": "waterLevel",
10421
+ "valueType": "number"
10422
+ },
10423
+ {
10424
+ "path": "waterVolume",
10425
+ "valueType": "number"
10426
+ },
10427
+ {
10428
+ "path": "empty",
10429
+ "valueType": "boolean"
10430
+ },
10431
+ {
10432
+ "path": "pumpRunning",
10433
+ "valueType": "boolean"
10434
+ },
10435
+ {
10436
+ "path": "pumpIntervalEnabled",
10437
+ "valueType": "boolean"
10438
+ },
10439
+ {
10440
+ "path": "pumpOnTime",
10441
+ "valueType": "number"
10442
+ },
10443
+ {
10444
+ "path": "pumpOffTime",
10445
+ "valueType": "number"
10446
+ },
10447
+ {
10448
+ "path": "lastDrink",
10449
+ "valueType": "number"
10450
+ },
10451
+ {
10452
+ "path": "currentHourDrink",
10453
+ "valueType": "number"
10454
+ },
10455
+ {
10456
+ "path": "last24hDrink",
10457
+ "valueType": "number"
10458
+ },
10459
+ {
10460
+ "path": "todayDrink",
10461
+ "valueType": "number"
10462
+ },
10463
+ {
10464
+ "path": "error",
10465
+ "valueType": "string"
10466
+ }
10467
+ ],
10342
10468
  "power-meter": [
10343
10469
  {
10344
10470
  "path": "power",
@@ -10876,7 +11002,7 @@ object({
10876
11002
  params: record(string(), union([string(), number()])).optional()
10877
11003
  });
10878
11004
  //#endregion
10879
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11005
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10880
11006
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10881
11007
  DeviceType["Camera"] = "camera";
10882
11008
  DeviceType["Hub"] = "hub";
@@ -10968,6 +11094,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10968
11094
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10969
11095
  * integrations sharing the same food/desiccant/hopper surface. */
10970
11096
  DeviceType["PetFeeder"] = "pet-feeder";
11097
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11098
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11099
+ * of a container and is BUILT as a composed device (D967). */
11100
+ DeviceType["PetFountain"] = "pet-fountain";
10971
11101
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10972
11102
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10973
11103
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11238,7 +11368,7 @@ function errMsg(err) {
11238
11368
  return String(err);
11239
11369
  }
11240
11370
  //#endregion
11241
- //#region ../types/dist/composition-bIBW5_13.mjs
11371
+ //#region ../types/dist/composition-kSuOifuG.mjs
11242
11372
  /**
11243
11373
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11244
11374
  *
@@ -11286,11 +11416,11 @@ function errMsg(err) {
11286
11416
  */
11287
11417
  /** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
11288
11418
  var FFMPEG_RELEASE = "8.1.2-5";
11289
- var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
11419
+ var BASE$1 = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
11290
11420
  function portable(target) {
11291
- return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
11421
+ return `${BASE$1}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
11292
11422
  }
11293
- portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE}${FFMPEG_RELEASE}`;
11423
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE$1}${FFMPEG_RELEASE}`;
11294
11424
  /**
11295
11425
  * THE table "which boolean of this device type counts as HIGH", and the
11296
11426
  * pure evaluator of its EDGE.
@@ -12323,12 +12453,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12323
12453
  ])
12324
12454
  })
12325
12455
  ]);
12326
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12456
+ var forwardArgUnion = union([
12457
+ string(),
12458
+ number(),
12459
+ boolean(),
12460
+ strictObject({ ["$input"]: string().min(1) }),
12461
+ record(string().min(1), lazy(() => forwardArgUnion))
12462
+ ]);
12463
+ var CompositionForwardArgSchema = forwardArgUnion;
12464
+ /**
12465
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12466
+ * call of `cap.method` on `source`. Without `args` the built method's input
12467
+ * passes through minus `deviceId`; with `args` the template IS the source's
12468
+ * whole input. `deviceId` is always the source's own, never an argument.
12469
+ */
12470
+ var CompositionForwardCommandSchema = object({
12327
12471
  kind: literal("forward"),
12328
12472
  source: CompositionSourceRefSchema,
12329
12473
  cap: string().min(1),
12330
- method: string().min(1)
12331
- }), CompositionCodeSourceSchema]);
12474
+ method: string().min(1),
12475
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12476
+ });
12477
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12332
12478
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12333
12479
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12334
12480
  /**
@@ -12338,7 +12484,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12338
12484
  */
12339
12485
  var CompositionItemEntrySchema = object({
12340
12486
  label: string().min(1).max(80),
12341
- fields: record(string().min(1), CompositionFieldSourceSchema)
12487
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12488
+ /**
12489
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12490
+ * is resettable and a `reset` call naming its key forwards here. The item key
12491
+ * selects the command, so it is never an argument: there is no pass-through.
12492
+ */
12493
+ reset: CompositionForwardCommandSchema.optional()
12342
12494
  });
12343
12495
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12344
12496
  kind: literal("fields"),
@@ -12350,7 +12502,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12350
12502
  * owns the WHOLE array field: the composed value holds exactly these items.
12351
12503
  */
12352
12504
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12353
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12505
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12354
12506
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12355
12507
  }), object({
12356
12508
  kind: literal("passthrough"),
@@ -16407,6 +16559,449 @@ object({
16407
16559
  lastError: string().nullable()
16408
16560
  });
16409
16561
  /**
16562
+ * notification-output — canonical, capability-gated notification delivery.
16563
+ *
16564
+ * Apprise-derived model (see
16565
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16566
+ * callers emit ONE canonical `Notification`; each provider declares a
16567
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16568
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16569
+ * message to what the kind supports — callers never special-case a service.
16570
+ *
16571
+ * DESIGN DECISIONS (locked):
16572
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16573
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16574
+ * cap. Rationale: the admin UI needs one uniform surface across the
16575
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16576
+ * alternative would fork the UI per addon and cannot host the
16577
+ * discovery→adopt flow.
16578
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16579
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16580
+ * registered provider (notifiers addon + HA addon) so one catalog is
16581
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16582
+ * `addonId` the generated collection router extracts from the call input.
16583
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16584
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16585
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16586
+ * base64 fallback needed.
16587
+ *
16588
+ * TODO (deferred, closed-set change — separate decision): add
16589
+ * `providerKind: 'notify'` so notification providers surface on the unified
16590
+ * admin "Integrations" page.
16591
+ */
16592
+ /**
16593
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16594
+ * adapter picks what it supports and the degrade engine filters the rest.
16595
+ */
16596
+ var AttachmentMediaTypeSchema = _enum([
16597
+ "image",
16598
+ "video",
16599
+ "gif",
16600
+ "audio",
16601
+ "icon"
16602
+ ]);
16603
+ /**
16604
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16605
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16606
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16607
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16608
+ */
16609
+ var AttachmentSchema = object({
16610
+ mediaType: AttachmentMediaTypeSchema,
16611
+ url: string().optional(),
16612
+ bytes: _instanceof(Uint8Array).optional(),
16613
+ mime: string().optional(),
16614
+ name: string().optional()
16615
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16616
+ var NotificationFormatSchema = _enum([
16617
+ "text",
16618
+ "markdown",
16619
+ "html"
16620
+ ]);
16621
+ /**
16622
+ * The CLOSED icon vocabulary an action button may use.
16623
+ *
16624
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16625
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16626
+ * notices — the same class of gap as a zone vocabulary nothing produced
16627
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16628
+ * declares `actionIcons: false` and the degrade engine strips the field.
16629
+ *
16630
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16631
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16632
+ * differently.
16633
+ *
16634
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16635
+ *
16636
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16637
+ * the whole set onto its own renderer's vocabulary through a
16638
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16639
+ * fallback, so adding a member here fails every adapter's build until someone
16640
+ * decides its glyph, which is the only place that decision can be made
16641
+ * honestly.
16642
+ *
16643
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16644
+ * `disarm` straight through; iOS feeds that string to
16645
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16646
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16647
+ * mapping, and "the field is documented" is not "the value renders".
16648
+ *
16649
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16650
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16651
+ * so an addon that emits a token the running hub does not know does not lose an
16652
+ * icon — its whole `send` fails Zod validation and the notification never
16653
+ * arrives. Never emit a new token from an addon before the train carrying it.
16654
+ */
16655
+ var NotificationActionIconSchema = _enum([
16656
+ "acknowledge",
16657
+ "dismiss",
16658
+ "silence",
16659
+ "snooze",
16660
+ "view",
16661
+ "play",
16662
+ "open",
16663
+ "close",
16664
+ "lock",
16665
+ "unlock",
16666
+ "arm",
16667
+ "arm-home",
16668
+ "arm-away",
16669
+ "arm-night",
16670
+ "disarm",
16671
+ "light",
16672
+ "alert",
16673
+ "camera"
16674
+ ]);
16675
+ /** A single tap-through action button. */
16676
+ var NotificationActionSchema = object({
16677
+ id: string(),
16678
+ label: string(),
16679
+ url: string().optional(),
16680
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16681
+ icon: NotificationActionIconSchema.optional(),
16682
+ /**
16683
+ * Renders in a warning style where the notifier supports it.
16684
+ *
16685
+ * A HINT, never a gate. The callback's authority is its token and nothing
16686
+ * else — see `notification-center/action-token.ts` for what that does and
16687
+ * does not buy.
16688
+ */
16689
+ destructive: boolean().optional(),
16690
+ /**
16691
+ * How the tap should REACH the url.
16692
+ *
16693
+ * `navigate` (absent, and every button authored before this field) opens it:
16694
+ * the phone leaves the notification and shows whatever the callback returns.
16695
+ * That is right for a button whose answer the operator wants to read.
16696
+ *
16697
+ * `background` fires it as a POST and stays put. It exists for the buttons
16698
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16699
+ * an answer to the notification, and being thrown into a browser tab to
16700
+ * confirm it costs more attention than the notification did. A backend that
16701
+ * cannot do a background call renders it as an ordinary link (the adapters
16702
+ * fall back rather than dropping the button), so this is a preference, never
16703
+ * a requirement.
16704
+ */
16705
+ mode: _enum(["navigate", "background"]).optional()
16706
+ });
16707
+ /** A delivery parameter value: what a notifier declared it takes. */
16708
+ var DeliveryParamValueSchema = union([
16709
+ string(),
16710
+ number(),
16711
+ boolean()
16712
+ ]);
16713
+ /**
16714
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16715
+ * addresses a target of that kind may set it; a kind that declares nothing is
16716
+ * never handed one. The admin UI derives its control from this descriptor —
16717
+ * no editor knows a service by name.
16718
+ *
16719
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16720
+ * run on the addon cap path).
16721
+ */
16722
+ var DeliveryParamSchema = object({
16723
+ key: string().min(1),
16724
+ label: string(),
16725
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16726
+ labelKey: string().optional(),
16727
+ type: _enum([
16728
+ "enum",
16729
+ "number",
16730
+ "boolean",
16731
+ "string"
16732
+ ]),
16733
+ /** `enum` only: the closed set of accepted values. */
16734
+ options: array(object({
16735
+ value: string(),
16736
+ label: string()
16737
+ })).optional(),
16738
+ /** `number` only. */
16739
+ min: number().optional(),
16740
+ max: number().optional(),
16741
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16742
+ default: DeliveryParamValueSchema.optional()
16743
+ });
16744
+ /**
16745
+ * The canonical notification. `body` is the only hard field (Apprise model).
16746
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16747
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16748
+ * the adapter maps this ordinal onto its native level. `level?` is an
16749
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16750
+ * `priority` for that one target.
16751
+ */
16752
+ var NotificationSchema = object({
16753
+ body: string(),
16754
+ title: string().optional(),
16755
+ format: NotificationFormatSchema.default("text"),
16756
+ priority: number().int().min(1).max(5).default(3),
16757
+ level: string().optional(),
16758
+ attachments: array(AttachmentSchema).optional(),
16759
+ clickUrl: string().optional(),
16760
+ actions: array(NotificationActionSchema).optional(),
16761
+ sound: string().optional(),
16762
+ ttl: number().optional(),
16763
+ tag: string().optional(),
16764
+ deviceId: number().optional(),
16765
+ eventId: string().optional(),
16766
+ metadata: record(string(), unknown()).optional(),
16767
+ /**
16768
+ * Values for the parameters the target's kind DECLARES in
16769
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16770
+ * declaration by the sender; a notifier reads only the keys it declared.
16771
+ */
16772
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16773
+ });
16774
+ /** One declared native severity/priority level for a kind. */
16775
+ var TargetKindLevelSchema = object({
16776
+ id: string(),
16777
+ label: string(),
16778
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16779
+ ordinal: number().int().min(1).max(5).nullable(),
16780
+ flags: object({
16781
+ critical: boolean().optional(),
16782
+ silent: boolean().optional(),
16783
+ noPush: boolean().optional()
16784
+ }).optional(),
16785
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16786
+ requires: array(string()).optional(),
16787
+ description: string().optional()
16788
+ });
16789
+ /** The full capability block consulted before dispatch. */
16790
+ var TargetKindCapsSchema = object({
16791
+ attachments: object({
16792
+ mediaTypes: array(AttachmentMediaTypeSchema),
16793
+ mode: _enum([
16794
+ "url",
16795
+ "bytes",
16796
+ "both"
16797
+ ]),
16798
+ max: number().int().nonnegative(),
16799
+ maxBytes: number().int().positive().optional()
16800
+ }),
16801
+ /** Max action buttons (0 = none). */
16802
+ actions: number().int().nonnegative(),
16803
+ /**
16804
+ * Whether this kind renders a per-action ICON.
16805
+ *
16806
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16807
+ * run on the addon cap path — three production failures in one day taught
16808
+ * this repo that once. Absent is read as false by the degrade engine, which
16809
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16810
+ * assumed and dropped costs the operator's trust in the field.
16811
+ */
16812
+ actionIcons: boolean().optional(),
16813
+ levels: array(TargetKindLevelSchema),
16814
+ format: array(NotificationFormatSchema),
16815
+ clickUrl: boolean(),
16816
+ sound: boolean(),
16817
+ ttl: boolean(),
16818
+ bodyMaxLen: number().int().positive()
16819
+ });
16820
+ /**
16821
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16822
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16823
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16824
+ * the union is large and not meant for runtime validation here; the exported
16825
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16826
+ */
16827
+ var ConfigSchemaPassthrough$3 = unknown();
16828
+ var TargetKindSchema = object({
16829
+ kind: string(),
16830
+ label: string(),
16831
+ icon: string(),
16832
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16833
+ addonId: string(),
16834
+ /**
16835
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16836
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16837
+ * when the addon bundles no icon for that kind — the client then falls back
16838
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16839
+ *
16840
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16841
+ * client, and a native client joins it onto its own hub base.
16842
+ *
16843
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16844
+ * field that survived only because the runtime cap-router forwards provider
16845
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16846
+ * its own Zod parse from stripping it, and the whole arrangement would have
16847
+ * broken silently the moment output validation was tightened anywhere.
16848
+ */
16849
+ iconUrl: string().optional(),
16850
+ /**
16851
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16852
+ *
16853
+ * The server knows this and therefore says it, because the client cannot
16854
+ * safely guess: a React-Native client renders SVG and raster through two
16855
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16856
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16857
+ * placeholder glyph for every vector icon while the web build looked fine.
16858
+ *
16859
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16860
+ * not been updated — a client that cannot determine the type should prefer
16861
+ * its raster path, which is the safe default for an unknown image.
16862
+ */
16863
+ iconMediaType: string().optional(),
16864
+ configSchema: ConfigSchemaPassthrough$3,
16865
+ /**
16866
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16867
+ * base URL, instance, chat, recipient). A save that changes any of them
16868
+ * while leaving a secret redacted is refused by the provider (D634), so an
16869
+ * editor uses this to ask for the secret on exactly those edits instead of
16870
+ * on every edit.
16871
+ *
16872
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16873
+ * kinds) and from a provider older than this field — an editor that finds
16874
+ * it absent must assume ANY edit may need the secret.
16875
+ */
16876
+ destinationKeys: array(string()).optional(),
16877
+ supportsDiscovery: boolean(),
16878
+ /**
16879
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16880
+ * Absent = none; the rule editor then offers nothing.
16881
+ */
16882
+ deliveryParams: array(DeliveryParamSchema).optional(),
16883
+ caps: TargetKindCapsSchema
16884
+ });
16885
+ /**
16886
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16887
+ * (return a presence marker only) when serving `listTargets` — never
16888
+ * round-trip a stored secret to the UI.
16889
+ */
16890
+ var TargetSchema = object({
16891
+ id: string(),
16892
+ name: string(),
16893
+ kind: string(),
16894
+ addonId: string(),
16895
+ enabled: boolean(),
16896
+ config: record(string(), unknown())
16897
+ });
16898
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16899
+ var DiscoveredTargetSchema = object({
16900
+ kind: string(),
16901
+ suggestedName: string(),
16902
+ config: record(string(), unknown())
16903
+ });
16904
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16905
+ var RenderedAsSchema = object({
16906
+ level: string(),
16907
+ format: NotificationFormatSchema,
16908
+ attachmentsSent: number().int().nonnegative(),
16909
+ actionsSent: number().int().nonnegative(),
16910
+ truncated: boolean(),
16911
+ dropped: array(string())
16912
+ });
16913
+ var SendResultSchema = object({
16914
+ success: boolean(),
16915
+ error: string().optional(),
16916
+ renderedAs: RenderedAsSchema.optional()
16917
+ });
16918
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16919
+ var TestResultSchema = SendResultSchema;
16920
+ /**
16921
+ * Pre-save credential probe against a partial (unsaved) target config.
16922
+ * `resolvedUrl` never includes secrets (token stripped from path).
16923
+ */
16924
+ var ProbeTargetConfigResultSchema = object({
16925
+ ok: boolean(),
16926
+ resolvedUrl: string(),
16927
+ httpStatus: number().int().optional(),
16928
+ stateInstance: string().optional(),
16929
+ error: string().optional()
16930
+ });
16931
+ var notificationOutputCapability = {
16932
+ name: "notification-output",
16933
+ scope: "system",
16934
+ mode: "collection",
16935
+ methods: {
16936
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
16937
+ listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
16938
+ /**
16939
+ * One target WITH secrets — admin edit / duplicate hydration only.
16940
+ * `listTargets` stays redacted; the editor loads this so password fields
16941
+ * are filled (masked in the input) and a save can round-trip the token
16942
+ * when a destination changes (Duplicate with a new chatId, etc.).
16943
+ */
16944
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
16945
+ discoverTargets: method(object({
16946
+ kind: string(),
16947
+ config: record(string(), unknown()).optional(),
16948
+ /**
16949
+ * When editing / duplicating, the blank password field never round-trips
16950
+ * the stored token. Pass the source target id so the provider can restore
16951
+ * secrets server-side before dialling (same merge as upsert).
16952
+ */
16953
+ targetId: string().optional()
16954
+ }), array(DiscoveredTargetSchema), { auth: "admin" }),
16955
+ probeTargetConfig: method(object({
16956
+ kind: string(),
16957
+ config: record(string(), unknown()),
16958
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
16959
+ targetId: string().optional()
16960
+ }), ProbeTargetConfigResultSchema, {
16961
+ kind: "mutation",
16962
+ auth: "admin"
16963
+ }),
16964
+ send: method(object({
16965
+ targetId: string(),
16966
+ notification: NotificationSchema
16967
+ }), SendResultSchema, {
16968
+ kind: "mutation",
16969
+ auth: "admin"
16970
+ }),
16971
+ testTarget: method(object({
16972
+ targetId: string(),
16973
+ sample: NotificationSchema.optional()
16974
+ }), TestResultSchema, {
16975
+ kind: "mutation",
16976
+ auth: "admin"
16977
+ }),
16978
+ upsertTarget: method(object({
16979
+ target: TargetSchema,
16980
+ /**
16981
+ * Duplicate flow: create a NEW target id while copying secrets from an
16982
+ * existing row. The UI never holds the stored token; blank / redacted
16983
+ * password fields are restored from this source (D634 still applies if
16984
+ * destinations diverge without a re-typed secret).
16985
+ */
16986
+ cloneSecretsFrom: string().optional()
16987
+ }), TargetSchema, {
16988
+ kind: "mutation",
16989
+ auth: "admin"
16990
+ }),
16991
+ deleteTarget: method(object({ targetId: string() }), _void(), {
16992
+ kind: "mutation",
16993
+ auth: "admin"
16994
+ }),
16995
+ setTargetEnabled: method(object({
16996
+ targetId: string(),
16997
+ enabled: boolean()
16998
+ }), _void(), {
16999
+ kind: "mutation",
17000
+ auth: "admin"
17001
+ })
17002
+ }
17003
+ };
17004
+ /**
16410
17005
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16411
17006
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
16412
17007
  *
@@ -16438,52 +17033,6 @@ var AddonPageInfoSchema = object({
16438
17033
  bundleUrl: string()
16439
17034
  });
16440
17035
  method(_void(), array(AddonPageInfoSchema).readonly());
16441
- /**
16442
- * `addon-pages-source` — collection cap exposing per-provider raw page
16443
- * declarations. Every addon that contributes a UI page registers a
16444
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16445
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16446
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16447
- * that admin-ui consumes.
16448
- *
16449
- * The split exists because the public listing has a different output
16450
- * shape than the per-provider raw declarations, and we want both ends
16451
- * to flow through codegen instead of relying on a hand-written wrapper.
16452
- */
16453
- var AddonPageDeclarationSchema = object({
16454
- id: string(),
16455
- label: string(),
16456
- icon: string(),
16457
- path: string(),
16458
- /**
16459
- * Module Federation remote name — must match the `name` field on the
16460
- * page addon's `federation()` plugin config. Used by admin-ui's
16461
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16462
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16463
- * contain hyphens).
16464
- */
16465
- remoteName: string(),
16466
- /**
16467
- * Bundle filename inside the addon's `dist/` dir served at
16468
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16469
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16470
- * the static-file route can compute an mtime-based cache-buster URL
16471
- * without a separate filesystem stat.
16472
- */
16473
- bundle: string(),
16474
- /**
16475
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16476
- * `'cluster'`, `'administration'` — the page renders inside that group.
16477
- * Any OTHER string creates (or joins) a custom section rendered after
16478
- * the built-in groups; its label comes from `sectionLabel` (first
16479
- * declaration wins), falling back to the id. Absent → the legacy
16480
- * "Addon Pages" group.
16481
- */
16482
- section: string().optional(),
16483
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16484
- sectionLabel: string().optional()
16485
- });
16486
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
16487
17036
  var AddonHttpRouteSchema = object({
16488
17037
  method: _enum([
16489
17038
  "GET",
@@ -19200,7 +19749,11 @@ var CompositionProblemSchema = object({
19200
19749
  "mixed-quantity",
19201
19750
  "helper-mismatch",
19202
19751
  "member-constraint",
19203
- "retention"
19752
+ "retention",
19753
+ "command-method-unknown",
19754
+ "command-source-unknown",
19755
+ "command-input-unknown",
19756
+ "command-input-missing"
19204
19757
  ]),
19205
19758
  path: string(),
19206
19759
  message: string()
@@ -19239,7 +19792,9 @@ var CompositionFieldRoleSchema = _enum([
19239
19792
  var CompositionFieldItemSchema = object({
19240
19793
  arrayPath: string(),
19241
19794
  key: string(),
19242
- label: string()
19795
+ label: string(),
19796
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19797
+ resettable: boolean().optional()
19243
19798
  });
19244
19799
  var CompositionFieldPlanSchema = object({
19245
19800
  cap: string(),
@@ -19444,7 +19999,7 @@ Object.freeze({
19444
19999
  * One catalog row per base reading a group helper can build (D944 §3).
19445
20000
  * Data, not a form default written into the block.
19446
20001
  */
19447
- var NOW$2 = {
20002
+ var NOW$3 = {
19448
20003
  fieldPath: "lastFetchedAt",
19449
20004
  expr: "now"
19450
20005
  };
@@ -19496,7 +20051,7 @@ var GROUP_TEMPLATES = [
19496
20051
  quantity: "temperature",
19497
20052
  defaultAggregate: "mean",
19498
20053
  empty: "null",
19499
- companions: [NOW$2],
20054
+ companions: [NOW$3],
19500
20055
  alsoFrom: {
19501
20056
  deviceTypes: CLIMATE_DEVICES,
19502
20057
  accept: {
@@ -19514,7 +20069,7 @@ var GROUP_TEMPLATES = [
19514
20069
  quantity: "humidity",
19515
20070
  defaultAggregate: "mean",
19516
20071
  empty: "null",
19517
- companions: [NOW$2],
20072
+ companions: [NOW$3],
19518
20073
  alsoFrom: {
19519
20074
  deviceTypes: CLIMATE_DEVICES,
19520
20075
  accept: {
@@ -19532,7 +20087,7 @@ var GROUP_TEMPLATES = [
19532
20087
  quantity: "pressure",
19533
20088
  defaultAggregate: "mean",
19534
20089
  empty: "null",
19535
- companions: [NOW$2]
20090
+ companions: [NOW$3]
19536
20091
  }),
19537
20092
  numeric({
19538
20093
  id: "illuminance",
@@ -19543,7 +20098,7 @@ var GROUP_TEMPLATES = [
19543
20098
  quantity: "illuminance",
19544
20099
  defaultAggregate: "mean",
19545
20100
  empty: "null",
19546
- companions: [NOW$2]
20101
+ companions: [NOW$3]
19547
20102
  }),
19548
20103
  numeric({
19549
20104
  id: "power",
@@ -19554,7 +20109,7 @@ var GROUP_TEMPLATES = [
19554
20109
  quantity: "power",
19555
20110
  defaultAggregate: "sum",
19556
20111
  empty: "omit",
19557
- companions: [NOW$2]
20112
+ companions: [NOW$3]
19558
20113
  }),
19559
20114
  numeric({
19560
20115
  id: "energy",
@@ -19565,7 +20120,7 @@ var GROUP_TEMPLATES = [
19565
20120
  quantity: "energy",
19566
20121
  defaultAggregate: "sum",
19567
20122
  empty: "omit",
19568
- companions: [NOW$2]
20123
+ companions: [NOW$3]
19569
20124
  }),
19570
20125
  numeric({
19571
20126
  id: "battery",
@@ -19600,7 +20155,7 @@ var GROUP_TEMPLATES = [
19600
20155
  quantity: "device",
19601
20156
  defaultAggregate: "mean",
19602
20157
  empty: "null",
19603
- companions: [NOW$2]
20158
+ companions: [NOW$3]
19604
20159
  }),
19605
20160
  binary({
19606
20161
  id: "motion",
@@ -22074,12 +22629,48 @@ var ExposeInputSchema = object({
22074
22629
  capabilities: array(string()).optional()
22075
22630
  });
22076
22631
  var UnexposeInputSchema = object({ deviceId: string() });
22632
+ /**
22633
+ * What an exporter may expose besides devices (D968). An action and a
22634
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
22635
+ * exporter's own selection, not the device registry.
22636
+ */
22637
+ var ExportEntityKindSchema = _enum([
22638
+ "device",
22639
+ "action",
22640
+ "notifier"
22641
+ ]);
22642
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
22643
+ object({
22644
+ kind: literal("device"),
22645
+ deviceId: string()
22646
+ }),
22647
+ object({
22648
+ kind: literal("action"),
22649
+ actionId: string().min(1)
22650
+ }),
22651
+ object({
22652
+ kind: literal("notifier"),
22653
+ targetId: string().min(1)
22654
+ })
22655
+ ]);
22656
+ var ExposedEntitySchema = object({
22657
+ entity: ExportEntityRefSchema,
22658
+ exposedAs: string().optional()
22659
+ });
22660
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22661
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22077
22662
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
22078
22663
  kind: "mutation",
22079
22664
  auth: "admin"
22080
22665
  }), method(UnexposeInputSchema, _void(), {
22081
22666
  kind: "mutation",
22082
22667
  auth: "admin"
22668
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
22669
+ kind: "mutation",
22670
+ auth: "admin"
22671
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
22672
+ kind: "mutation",
22673
+ auth: "admin"
22083
22674
  });
22084
22675
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
22085
22676
  var ICON_CATEGORY_IDS = [
@@ -44029,7 +44620,7 @@ var LlmProviderWidgetSchema = object({
44029
44620
  /** ConfigUISchema tree passed through untyped on the wire (the
44030
44621
  * notification-output `ConfigSchemaPassthrough` precedent at
44031
44622
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
44032
- var ConfigSchemaPassthrough$3 = unknown();
44623
+ var ConfigSchemaPassthrough$2 = unknown();
44033
44624
  var LlmProfileKindDescriptorSchema = object({
44034
44625
  kind: LlmProfileKindSchema,
44035
44626
  label: string(),
@@ -44038,9 +44629,9 @@ var LlmProfileKindDescriptorSchema = object({
44038
44629
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
44039
44630
  addonId: string(),
44040
44631
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
44041
- connectionSchema: ConfigSchemaPassthrough$3,
44632
+ connectionSchema: ConfigSchemaPassthrough$2,
44042
44633
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
44043
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
44634
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
44044
44635
  /** The kind's defaults; a model from `listModels` may override any of them. */
44045
44636
  features: LlmModelFeaturesSchema,
44046
44637
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44566,10 +45157,10 @@ var CpuBreakdownSchema = object({
44566
45157
  cores: number()
44567
45158
  });
44568
45159
  var MemoryInfoSchema = object({
44569
- percent: number(),
45160
+ percent: number().nullable(),
44570
45161
  totalBytes: number(),
44571
- usedBytes: number(),
44572
- availableBytes: number(),
45162
+ usedBytes: number().nullable(),
45163
+ availableBytes: number().nullable(),
44573
45164
  swapUsedBytes: number(),
44574
45165
  swapTotalBytes: number()
44575
45166
  });
@@ -44873,8 +45464,9 @@ var GetLoadSeriesInputSchema = object({
44873
45464
  });
44874
45465
  var SystemMetricsSchema = object({
44875
45466
  cpuPercent: number(),
44876
- memoryPercent: number(),
44877
- memoryUsedMB: number(),
45467
+ /** `null` when the memory sample failed or was not taken yet (D393). */
45468
+ memoryPercent: number().nullable(),
45469
+ memoryUsedMB: number().nullable(),
44878
45470
  memoryTotalMB: number(),
44879
45471
  diskPercent: number().optional(),
44880
45472
  temperature: number().optional(),
@@ -45003,556 +45595,113 @@ var BrokerInfoSchema$1 = object({
45003
45595
  * consumer can pass them as `mqtt.connect(url, { username, password })`
45004
45596
  * instead of stuffing creds into the URL (which leaks them into logs).
45005
45597
  */
45006
- var BrokerConnectionDetailsSchema = object({
45007
- url: string(),
45008
- username: string().optional(),
45009
- password: string().optional(),
45010
- /**
45011
- * Suggested prefix for `clientId`. Each consumer should suffix this
45012
- * with its own discriminator (addon id, instance id) so reconnects
45013
- * don't kick each other off (MQTT spec: clientId must be unique per
45014
- * broker).
45015
- */
45016
- clientIdPrefix: string().optional()
45017
- });
45018
- var AddBrokerInputSchema = object({
45019
- name: string().min(1),
45020
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45021
- username: string().optional(),
45022
- password: string().optional(),
45023
- clientIdPrefix: string().optional()
45024
- });
45025
- var AddBrokerResultSchema = object({ id: string() });
45026
- var IdInputSchema = object({ id: string() });
45027
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
45028
- ok: literal(true),
45029
- latencyMs: number()
45030
- }), object({
45031
- ok: literal(false),
45032
- error: string()
45033
- })]);
45034
- var StartEmbeddedInputSchema = object({
45035
- port: number().int().min(1).max(65535).default(1883),
45036
- /** Allow anonymous connect (no username/password). Default: false. */
45037
- allowAnonymous: boolean().default(false),
45038
- /** Optional shared username/password for clients. */
45039
- username: string().optional(),
45040
- password: string().optional()
45041
- });
45042
- var StartEmbeddedResultSchema = object({
45043
- id: string(),
45044
- url: string()
45045
- });
45046
- var StatusSchema = object({
45047
- brokerCount: number(),
45048
- embeddedRunning: boolean()
45049
- });
45050
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45051
- kind: "mutation",
45052
- auth: "admin"
45053
- }), method(IdInputSchema, _void(), {
45054
- kind: "mutation",
45055
- auth: "admin"
45056
- }), method(IdInputSchema, TestResultSchema$1, {
45057
- kind: "mutation",
45058
- auth: "admin"
45059
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45060
- kind: "mutation",
45061
- auth: "admin"
45062
- }), method(IdInputSchema, _void(), {
45063
- kind: "mutation",
45064
- auth: "admin"
45065
- }), method(_void(), StatusSchema);
45066
- var NetworkEndpointSchema = object({
45067
- url: string(),
45068
- hostname: string(),
45069
- port: number(),
45070
- protocol: _enum(["http", "https"]),
45071
- /**
45072
- * Whether this address survives a restart of its provider. `false` = it is
45073
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45074
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
45075
- * it stays visible and an operator may still flag it. Absent = stable: every
45076
- * provider except a Quick tunnel owns a name that does not change.
45077
- */
45078
- stable: boolean().optional()
45079
- });
45080
- object({
45081
- url: string(),
45082
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45083
- stable: boolean().optional(),
45084
- updated: boolean().optional()
45085
- });
45086
- var NetworkAccessStatusSchema = object({
45087
- connected: boolean(),
45088
- endpoint: NetworkEndpointSchema.nullable(),
45089
- error: string().optional()
45090
- });
45091
- /**
45092
- * Optional, richer endpoint shape returned by providers that expose
45093
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
45094
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45095
- * the originating provider config (mode + sourcePort) so the
45096
- * orchestrator UI can label rows distinctly. Providers that expose only
45097
- * one endpoint just omit `listEndpoints` from their provider impl.
45098
- */
45099
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45100
- /**
45101
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
45102
- * the orchestrator can dedupe across `listEndpoints` polls.
45103
- */
45104
- id: string(),
45105
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45106
- label: string(),
45107
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45108
- mode: string().optional(),
45109
- /** Originating local port the ingress fronts (informational). */
45110
- sourcePort: number().optional()
45111
- });
45112
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45113
- /**
45114
- * notification-output — canonical, capability-gated notification delivery.
45115
- *
45116
- * Apprise-derived model (see
45117
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
45118
- * callers emit ONE canonical `Notification`; each provider declares a
45119
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
45120
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
45121
- * message to what the kind supports — callers never special-case a service.
45122
- *
45123
- * DESIGN DECISIONS (locked):
45124
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
45125
- * `setTargetEnabled`), each provider persisting via the `settings-store`
45126
- * cap. Rationale: the admin UI needs one uniform surface across the
45127
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
45128
- * alternative would fork the UI per addon and cannot host the
45129
- * discovery→adopt flow.
45130
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
45131
- * the generated cap-mount auto-`concatCollection`-fans them across every
45132
- * registered provider (notifiers addon + HA addon) so one catalog is
45133
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
45134
- * `addonId` the generated collection router extracts from the call input.
45135
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
45136
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
45137
- * `storage` / `storage-provider` / `recording` caps over the same path. No
45138
- * base64 fallback needed.
45139
- *
45140
- * TODO (deferred, closed-set change — separate decision): add
45141
- * `providerKind: 'notify'` so notification providers surface on the unified
45142
- * admin "Integrations" page.
45143
- */
45144
- /**
45145
- * Zentik-derived typed-media enum — the superset across every kind. Each
45146
- * adapter picks what it supports and the degrade engine filters the rest.
45147
- */
45148
- var AttachmentMediaTypeSchema = _enum([
45149
- "image",
45150
- "video",
45151
- "gif",
45152
- "audio",
45153
- "icon"
45154
- ]);
45155
- /**
45156
- * A single attachment. Exactly one of `url` (remote source, most adapters
45157
- * prefer this) or `bytes` (inline source; required for Pushover-style
45158
- * bytes-only kinds) MUST be present — the degrade engine expresses a
45159
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
45160
- */
45161
- var AttachmentSchema = object({
45162
- mediaType: AttachmentMediaTypeSchema,
45163
- url: string().optional(),
45164
- bytes: _instanceof(Uint8Array).optional(),
45165
- mime: string().optional(),
45166
- name: string().optional()
45167
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
45168
- var NotificationFormatSchema = _enum([
45169
- "text",
45170
- "markdown",
45171
- "html"
45172
- ]);
45173
- /**
45174
- * The CLOSED icon vocabulary an action button may use.
45175
- *
45176
- * A closed set, not a free string, and that is the whole point: an arbitrary
45177
- * icon name is one that ntfy renders, zentik silently drops, and nobody
45178
- * notices — the same class of gap as a zone vocabulary nothing produced
45179
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
45180
- * declares `actionIcons: false` and the degrade engine strips the field.
45181
- *
45182
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
45183
- * renderer's icon set; "acknowledge" survives an adapter that draws it
45184
- * differently.
45185
- *
45186
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
45187
- *
45188
- * These names are for US. **No adapter may forward one verbatim.** Each maps
45189
- * the whole set onto its own renderer's vocabulary through a
45190
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
45191
- * fallback, so adding a member here fails every adapter's build until someone
45192
- * decides its glyph, which is the only place that decision can be made
45193
- * honestly.
45194
- *
45195
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
45196
- * `disarm` straight through; iOS feeds that string to
45197
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
45198
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
45199
- * mapping, and "the field is documented" is not "the value renders".
45200
- *
45201
- * Adding a member is TRAIN-BOUND. The enum lives in the published
45202
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
45203
- * so an addon that emits a token the running hub does not know does not lose an
45204
- * icon — its whole `send` fails Zod validation and the notification never
45205
- * arrives. Never emit a new token from an addon before the train carrying it.
45206
- */
45207
- var NotificationActionIconSchema = _enum([
45208
- "acknowledge",
45209
- "dismiss",
45210
- "silence",
45211
- "snooze",
45212
- "view",
45213
- "play",
45214
- "open",
45215
- "close",
45216
- "lock",
45217
- "unlock",
45218
- "arm",
45219
- "arm-home",
45220
- "arm-away",
45221
- "arm-night",
45222
- "disarm",
45223
- "light",
45224
- "alert",
45225
- "camera"
45226
- ]);
45227
- /** A single tap-through action button. */
45228
- var NotificationActionSchema = object({
45229
- id: string(),
45230
- label: string(),
45231
- url: string().optional(),
45232
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
45233
- icon: NotificationActionIconSchema.optional(),
45234
- /**
45235
- * Renders in a warning style where the notifier supports it.
45236
- *
45237
- * A HINT, never a gate. The callback's authority is its token and nothing
45238
- * else — see `notification-center/action-token.ts` for what that does and
45239
- * does not buy.
45240
- */
45241
- destructive: boolean().optional(),
45242
- /**
45243
- * How the tap should REACH the url.
45244
- *
45245
- * `navigate` (absent, and every button authored before this field) opens it:
45246
- * the phone leaves the notification and shows whatever the callback returns.
45247
- * That is right for a button whose answer the operator wants to read.
45248
- *
45249
- * `background` fires it as a POST and stays put. It exists for the buttons
45250
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
45251
- * an answer to the notification, and being thrown into a browser tab to
45252
- * confirm it costs more attention than the notification did. A backend that
45253
- * cannot do a background call renders it as an ordinary link (the adapters
45254
- * fall back rather than dropping the button), so this is a preference, never
45255
- * a requirement.
45256
- */
45257
- mode: _enum(["navigate", "background"]).optional()
45258
- });
45259
- /** A delivery parameter value: what a notifier declared it takes. */
45260
- var DeliveryParamValueSchema = union([
45261
- string(),
45262
- number(),
45263
- boolean()
45264
- ]);
45265
- /**
45266
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
45267
- * addresses a target of that kind may set it; a kind that declares nothing is
45268
- * never handed one. The admin UI derives its control from this descriptor —
45269
- * no editor knows a service by name.
45270
- *
45271
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
45272
- * run on the addon cap path).
45273
- */
45274
- var DeliveryParamSchema = object({
45275
- key: string().min(1),
45276
- label: string(),
45277
- /** i18n key of the label, resolved by the declaring addon's namespace. */
45278
- labelKey: string().optional(),
45279
- type: _enum([
45280
- "enum",
45281
- "number",
45282
- "boolean",
45283
- "string"
45284
- ]),
45285
- /** `enum` only: the closed set of accepted values. */
45286
- options: array(object({
45287
- value: string(),
45288
- label: string()
45289
- })).optional(),
45290
- /** `number` only. */
45291
- min: number().optional(),
45292
- max: number().optional(),
45293
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
45294
- default: DeliveryParamValueSchema.optional()
45295
- });
45296
- /**
45297
- * The canonical notification. `body` is the only hard field (Apprise model).
45298
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
45299
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
45300
- * the adapter maps this ordinal onto its native level. `level?` is an
45301
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
45302
- * `priority` for that one target.
45303
- */
45304
- var NotificationSchema = object({
45305
- body: string(),
45306
- title: string().optional(),
45307
- format: NotificationFormatSchema.default("text"),
45308
- priority: number().int().min(1).max(5).default(3),
45309
- level: string().optional(),
45310
- attachments: array(AttachmentSchema).optional(),
45311
- clickUrl: string().optional(),
45312
- actions: array(NotificationActionSchema).optional(),
45313
- sound: string().optional(),
45314
- ttl: number().optional(),
45315
- tag: string().optional(),
45316
- deviceId: number().optional(),
45317
- eventId: string().optional(),
45318
- metadata: record(string(), unknown()).optional(),
45319
- /**
45320
- * Values for the parameters the target's kind DECLARES in
45321
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45322
- * declaration by the sender; a notifier reads only the keys it declared.
45323
- */
45324
- delivery: record(string(), DeliveryParamValueSchema).optional()
45325
- });
45326
- /** One declared native severity/priority level for a kind. */
45327
- var TargetKindLevelSchema = object({
45328
- id: string(),
45329
- label: string(),
45330
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
45331
- ordinal: number().int().min(1).max(5).nullable(),
45332
- flags: object({
45333
- critical: boolean().optional(),
45334
- silent: boolean().optional(),
45335
- noPush: boolean().optional()
45336
- }).optional(),
45337
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
45338
- requires: array(string()).optional(),
45339
- description: string().optional()
45340
- });
45341
- /** The full capability block consulted before dispatch. */
45342
- var TargetKindCapsSchema = object({
45343
- attachments: object({
45344
- mediaTypes: array(AttachmentMediaTypeSchema),
45345
- mode: _enum([
45346
- "url",
45347
- "bytes",
45348
- "both"
45349
- ]),
45350
- max: number().int().nonnegative(),
45351
- maxBytes: number().int().positive().optional()
45352
- }),
45353
- /** Max action buttons (0 = none). */
45354
- actions: number().int().nonnegative(),
45355
- /**
45356
- * Whether this kind renders a per-action ICON.
45357
- *
45358
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
45359
- * run on the addon cap path — three production failures in one day taught
45360
- * this repo that once. Absent is read as false by the degrade engine, which
45361
- * is the safe direction: an icon that is not rendered costs nothing, an icon
45362
- * assumed and dropped costs the operator's trust in the field.
45363
- */
45364
- actionIcons: boolean().optional(),
45365
- levels: array(TargetKindLevelSchema),
45366
- format: array(NotificationFormatSchema),
45367
- clickUrl: boolean(),
45368
- sound: boolean(),
45369
- ttl: boolean(),
45370
- bodyMaxLen: number().int().positive()
45371
- });
45372
- /**
45373
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
45374
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
45375
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
45376
- * the union is large and not meant for runtime validation here; the exported
45377
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
45378
- */
45379
- var ConfigSchemaPassthrough$2 = unknown();
45380
- var TargetKindSchema = object({
45381
- kind: string(),
45382
- label: string(),
45383
- icon: string(),
45384
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
45385
- addonId: string(),
45386
- /**
45387
- * URL of the kind's bundled BRAND icon, served by the providing addon over
45388
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
45389
- * when the addon bundles no icon for that kind — the client then falls back
45390
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
45391
- *
45392
- * Root-relative on purpose: it resolves against whatever origin serves a web
45393
- * client, and a native client joins it onto its own hub base.
45394
- *
45395
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
45396
- * field that survived only because the runtime cap-router forwards provider
45397
- * output verbatim — so every consumer had to re-declare it by hand to stop
45398
- * its own Zod parse from stripping it, and the whole arrangement would have
45399
- * broken silently the moment output validation was tightened anywhere.
45400
- */
45401
- iconUrl: string().optional(),
45402
- /**
45403
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
45404
- *
45405
- * The server knows this and therefore says it, because the client cannot
45406
- * safely guess: a React-Native client renders SVG and raster through two
45407
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
45408
- * not decode SVG on iOS/Android), so without this it silently fell back to a
45409
- * placeholder glyph for every vector icon while the web build looked fine.
45410
- *
45411
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
45412
- * not been updated — a client that cannot determine the type should prefer
45413
- * its raster path, which is the safe default for an unknown image.
45414
- */
45415
- iconMediaType: string().optional(),
45416
- configSchema: ConfigSchemaPassthrough$2,
45417
- /**
45418
- * The config keys that decide WHERE this kind's secrets are sent (server,
45419
- * base URL, instance, chat, recipient). A save that changes any of them
45420
- * while leaving a secret redacted is refused by the provider (D634), so an
45421
- * editor uses this to ask for the secret on exactly those edits instead of
45422
- * on every edit.
45423
- *
45424
- * Optional: absent from a provider that holds no secret (the Home Assistant
45425
- * kinds) and from a provider older than this field — an editor that finds
45426
- * it absent must assume ANY edit may need the secret.
45427
- */
45428
- destinationKeys: array(string()).optional(),
45429
- supportsDiscovery: boolean(),
45598
+ var BrokerConnectionDetailsSchema = object({
45599
+ url: string(),
45600
+ username: string().optional(),
45601
+ password: string().optional(),
45430
45602
  /**
45431
- * Extra delivery parameters a rule may set for a target of this kind (D938).
45432
- * Absent = none; the rule editor then offers nothing.
45603
+ * Suggested prefix for `clientId`. Each consumer should suffix this
45604
+ * with its own discriminator (addon id, instance id) so reconnects
45605
+ * don't kick each other off (MQTT spec: clientId must be unique per
45606
+ * broker).
45433
45607
  */
45434
- deliveryParams: array(DeliveryParamSchema).optional(),
45435
- caps: TargetKindCapsSchema
45608
+ clientIdPrefix: string().optional()
45436
45609
  });
45437
- /**
45438
- * A persisted target. `config` holds secrets; providers REDACT secret fields
45439
- * (return a presence marker only) when serving `listTargets` — never
45440
- * round-trip a stored secret to the UI.
45441
- */
45442
- var TargetSchema = object({
45610
+ var AddBrokerInputSchema = object({
45611
+ name: string().min(1),
45612
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45613
+ username: string().optional(),
45614
+ password: string().optional(),
45615
+ clientIdPrefix: string().optional()
45616
+ });
45617
+ var AddBrokerResultSchema = object({ id: string() });
45618
+ var IdInputSchema = object({ id: string() });
45619
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
45620
+ ok: literal(true),
45621
+ latencyMs: number()
45622
+ }), object({
45623
+ ok: literal(false),
45624
+ error: string()
45625
+ })]);
45626
+ var StartEmbeddedInputSchema = object({
45627
+ port: number().int().min(1).max(65535).default(1883),
45628
+ /** Allow anonymous connect (no username/password). Default: false. */
45629
+ allowAnonymous: boolean().default(false),
45630
+ /** Optional shared username/password for clients. */
45631
+ username: string().optional(),
45632
+ password: string().optional()
45633
+ });
45634
+ var StartEmbeddedResultSchema = object({
45443
45635
  id: string(),
45444
- name: string(),
45445
- kind: string(),
45446
- addonId: string(),
45447
- enabled: boolean(),
45448
- config: record(string(), unknown())
45636
+ url: string()
45449
45637
  });
45450
- /** A discovery-surfaced candidate (config is partial + non-secret). */
45451
- var DiscoveredTargetSchema = object({
45452
- kind: string(),
45453
- suggestedName: string(),
45454
- config: record(string(), unknown())
45638
+ var StatusSchema = object({
45639
+ brokerCount: number(),
45640
+ embeddedRunning: boolean()
45455
45641
  });
45456
- /** The degrade engine's report — what was resolved / dropped / degraded. */
45457
- var RenderedAsSchema = object({
45458
- level: string(),
45459
- format: NotificationFormatSchema,
45460
- attachmentsSent: number().int().nonnegative(),
45461
- actionsSent: number().int().nonnegative(),
45462
- truncated: boolean(),
45463
- dropped: array(string())
45642
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45643
+ kind: "mutation",
45644
+ auth: "admin"
45645
+ }), method(IdInputSchema, _void(), {
45646
+ kind: "mutation",
45647
+ auth: "admin"
45648
+ }), method(IdInputSchema, TestResultSchema$1, {
45649
+ kind: "mutation",
45650
+ auth: "admin"
45651
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45652
+ kind: "mutation",
45653
+ auth: "admin"
45654
+ }), method(IdInputSchema, _void(), {
45655
+ kind: "mutation",
45656
+ auth: "admin"
45657
+ }), method(_void(), StatusSchema);
45658
+ var NetworkEndpointSchema = object({
45659
+ url: string(),
45660
+ hostname: string(),
45661
+ port: number(),
45662
+ protocol: _enum(["http", "https"]),
45663
+ /**
45664
+ * Whether this address survives a restart of its provider. `false` = it is
45665
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45666
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
45667
+ * it stays visible and an operator may still flag it. Absent = stable: every
45668
+ * provider except a Quick tunnel owns a name that does not change.
45669
+ */
45670
+ stable: boolean().optional()
45464
45671
  });
45465
- var SendResultSchema = object({
45466
- success: boolean(),
45467
- error: string().optional(),
45468
- renderedAs: RenderedAsSchema.optional()
45672
+ object({
45673
+ url: string(),
45674
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45675
+ stable: boolean().optional(),
45676
+ updated: boolean().optional()
45677
+ });
45678
+ var NetworkAccessStatusSchema = object({
45679
+ connected: boolean(),
45680
+ endpoint: NetworkEndpointSchema.nullable(),
45681
+ error: string().optional()
45469
45682
  });
45470
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
45471
- var TestResultSchema = SendResultSchema;
45472
45683
  /**
45473
- * Pre-save credential probe against a partial (unsaved) target config.
45474
- * `resolvedUrl` never includes secrets (token stripped from path).
45684
+ * Optional, richer endpoint shape returned by providers that expose
45685
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
45686
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45687
+ * the originating provider config (mode + sourcePort) so the
45688
+ * orchestrator UI can label rows distinctly. Providers that expose only
45689
+ * one endpoint just omit `listEndpoints` from their provider impl.
45475
45690
  */
45476
- var ProbeTargetConfigResultSchema = object({
45477
- ok: boolean(),
45478
- resolvedUrl: string(),
45479
- httpStatus: number().int().optional(),
45480
- stateInstance: string().optional(),
45481
- error: string().optional()
45691
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45692
+ /**
45693
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
45694
+ * the orchestrator can dedupe across `listEndpoints` polls.
45695
+ */
45696
+ id: string(),
45697
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45698
+ label: string(),
45699
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45700
+ mode: string().optional(),
45701
+ /** Originating local port the ingress fronts (informational). */
45702
+ sourcePort: number().optional()
45482
45703
  });
45483
- var notificationOutputCapability = {
45484
- name: "notification-output",
45485
- scope: "system",
45486
- mode: "collection",
45487
- methods: {
45488
- listTargetKinds: method(object({}), array(TargetKindSchema)),
45489
- listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
45490
- /**
45491
- * One target WITH secrets — admin edit / duplicate hydration only.
45492
- * `listTargets` stays redacted; the editor loads this so password fields
45493
- * are filled (masked in the input) and a save can round-trip the token
45494
- * when a destination changes (Duplicate with a new chatId, etc.).
45495
- */
45496
- getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
45497
- discoverTargets: method(object({
45498
- kind: string(),
45499
- config: record(string(), unknown()).optional(),
45500
- /**
45501
- * When editing / duplicating, the blank password field never round-trips
45502
- * the stored token. Pass the source target id so the provider can restore
45503
- * secrets server-side before dialling (same merge as upsert).
45504
- */
45505
- targetId: string().optional()
45506
- }), array(DiscoveredTargetSchema), { auth: "admin" }),
45507
- probeTargetConfig: method(object({
45508
- kind: string(),
45509
- config: record(string(), unknown()),
45510
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
45511
- targetId: string().optional()
45512
- }), ProbeTargetConfigResultSchema, {
45513
- kind: "mutation",
45514
- auth: "admin"
45515
- }),
45516
- send: method(object({
45517
- targetId: string(),
45518
- notification: NotificationSchema
45519
- }), SendResultSchema, {
45520
- kind: "mutation",
45521
- auth: "admin"
45522
- }),
45523
- testTarget: method(object({
45524
- targetId: string(),
45525
- sample: NotificationSchema.optional()
45526
- }), TestResultSchema, {
45527
- kind: "mutation",
45528
- auth: "admin"
45529
- }),
45530
- upsertTarget: method(object({
45531
- target: TargetSchema,
45532
- /**
45533
- * Duplicate flow: create a NEW target id while copying secrets from an
45534
- * existing row. The UI never holds the stored token; blank / redacted
45535
- * password fields are restored from this source (D634 still applies if
45536
- * destinations diverge without a re-typed secret).
45537
- */
45538
- cloneSecretsFrom: string().optional()
45539
- }), TargetSchema, {
45540
- kind: "mutation",
45541
- auth: "admin"
45542
- }),
45543
- deleteTarget: method(object({ targetId: string() }), _void(), {
45544
- kind: "mutation",
45545
- auth: "admin"
45546
- }),
45547
- setTargetEnabled: method(object({
45548
- targetId: string(),
45549
- enabled: boolean()
45550
- }), _void(), {
45551
- kind: "mutation",
45552
- auth: "admin"
45553
- })
45554
- }
45555
- };
45704
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45556
45705
  var AUDIO_MACRO_LABELS = [
45557
45706
  {
45558
45707
  id: "speech",
@@ -51997,14 +52146,6 @@ var EngineProvisioningSchema = object({
51997
52146
  */
51998
52147
  configIssues: array(string()).optional()
51999
52148
  });
52000
- var PipelineStepInputSchema = lazy(() => object({
52001
- addonId: string(),
52002
- modelId: string().optional(),
52003
- enabled: boolean().default(true),
52004
- children: array(PipelineStepInputSchema).optional(),
52005
- settings: record(string(), unknown()).optional(),
52006
- jumpDeviceKey: string().optional()
52007
- }));
52008
52149
  var ModelSubstitutionSchema = object({
52009
52150
  addonId: string(),
52010
52151
  chosen: string(),
@@ -52083,7 +52224,6 @@ var ConfigUISchemaBridge = custom();
52083
52224
  var ConfigUISchemaNullableBridge = custom();
52084
52225
  var InferenceCapabilitiesBridge = custom();
52085
52226
  var ModelAvailabilityListBridge = custom();
52086
- var PipelineRunResultBridge = custom();
52087
52227
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
52088
52228
  modelId: string(),
52089
52229
  settings: record(string(), unknown()).readonly()
@@ -55051,11 +55191,13 @@ var ImageContractSchema = object({
55051
55191
  * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
55052
55192
  * produced it.
55053
55193
  *
55054
- * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
55055
- * update is an ANNOUNCEMENT. The notification body carries the exact command
55056
- * for a `docker` shell and says the app restarts into the new build for
55057
- * `electron`, and that sentence is chosen from `kind` alone — a boolean
55058
- * nothing reads is the defect this repo keeps paying for.
55194
+ * There is deliberately NO `canSelfUpdate` boolean: a wrapper update is an
55195
+ * ANNOUNCEMENT. The notification body carries the exact command for a `docker`
55196
+ * shell and says the app restarts into the new build for `electron`, and that
55197
+ * sentence is chosen from `kind` alone — a boolean nothing reads is the defect
55198
+ * this repo keeps paying for. The one shell that CAN be acted on is the
55199
+ * Electron app, and only through `installDesktopUpdate` (see
55200
+ * {@link DesktopAppStatusSchema} for what the node reads from the app first).
55059
55201
  */
55060
55202
  var WrapperIdentitySchema = object({
55061
55203
  kind: _enum([
@@ -55103,6 +55245,49 @@ var ServerRollbackInfoSchema = object({
55103
55245
  reason: string()
55104
55246
  });
55105
55247
  /**
55248
+ * What the native desktop app (Electron) that hosts this node says about
55249
+ * itself, read from `<dataDir>/server-root/desktop-app-state.json` (ADR-0971).
55250
+ * Absent on the status = no desktop app, or it never wrote the file.
55251
+ *
55252
+ * `stale` is the honest answer to an app that stopped writing (closed, hung,
55253
+ * crashed): the update facts are then the LAST ones known, not current ones,
55254
+ * and a consumer must not read `update.available: false` from a stale record
55255
+ * as "no update". `stateAtMs` is when the app last wrote.
55256
+ */
55257
+ var DesktopAppStatusSchema = object({
55258
+ /** The Electron app's own version (not the node closure's). */
55259
+ version: string(),
55260
+ update: object({
55261
+ available: boolean(),
55262
+ /** The new build is downloaded and `installDesktopUpdate` can apply it. */
55263
+ downloaded: boolean(),
55264
+ version: string().nullable(),
55265
+ checking: boolean(),
55266
+ /** Why the app's last update check produced no answer; `null` when it did. */
55267
+ error: string().nullable()
55268
+ }),
55269
+ stateAtMs: number(),
55270
+ stale: boolean()
55271
+ });
55272
+ /**
55273
+ * Why `installDesktopUpdate` did nothing. Stable names a UI maps to a sentence:
55274
+ * - `not-electron` — this node does not run inside the desktop app.
55275
+ * - `no-app-state` — the app never published its state (or the file is unreadable).
55276
+ * - `app-state-stale` — the app stopped writing; it is probably not running.
55277
+ * - `not-downloaded` — no update is downloaded yet; nothing to install.
55278
+ */
55279
+ var InstallDesktopUpdateReasonSchema = _enum([
55280
+ "not-electron",
55281
+ "no-app-state",
55282
+ "app-state-stale",
55283
+ "not-downloaded"
55284
+ ]);
55285
+ var InstallDesktopUpdateResultSchema = object({
55286
+ accepted: boolean(),
55287
+ /** Non-null exactly when `accepted` is false. */
55288
+ reason: InstallDesktopUpdateReasonSchema.nullable()
55289
+ });
55290
+ /**
55106
55291
  * The runner boot check (D877): the runners this node PLANNED (hub: one per
55107
55292
  * `resolveRunnerId` over its forked addons; agent: the runners its process
55108
55293
  * service holds, because only the hub plans) against the states the process
@@ -55206,7 +55391,12 @@ var ServerPackageStatusSchema = object({
55206
55391
  * on a provider that predates it. Absent is "not checked yet", never "all
55207
55392
  * well": a consumer that needs the answer treats it as pending.
55208
55393
  */
55209
- runners: RunnerBootCheckSchema.optional()
55394
+ runners: RunnerBootCheckSchema.optional(),
55395
+ /**
55396
+ * The desktop app hosting this node (see {@link DesktopAppStatusSchema}).
55397
+ * Optional for version skew AND because most nodes have no desktop app.
55398
+ */
55399
+ desktopApp: DesktopAppStatusSchema.optional()
55210
55400
  });
55211
55401
  var ServerUpdateCheckResultSchema = object({
55212
55402
  packageName: string(),
@@ -55289,6 +55479,12 @@ var ServerUpdateActionResultSchema = object({
55289
55479
  */
55290
55480
  blockers: array(UpdateBlockerSchema).optional()
55291
55481
  });
55482
+ var LaunchOverridesSchema = object({
55483
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
55484
+ heapMb: number().int().nullable() });
55485
+ var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
55486
+ /** Always true: the override is read at launch, not by the running process. */
55487
+ restartRequired: boolean() });
55292
55488
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
55293
55489
  kind: "mutation",
55294
55490
  auth: "admin"
@@ -55305,12 +55501,18 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
55305
55501
  kind: "mutation",
55306
55502
  auth: "admin",
55307
55503
  timeoutMs: 16 * 6e4
55504
+ }), method(_void(), InstallDesktopUpdateResultSchema, {
55505
+ kind: "mutation",
55506
+ auth: "admin"
55308
55507
  }), method(_void(), ServerUpdateActionResultSchema, {
55309
55508
  kind: "mutation",
55310
55509
  auth: "admin"
55311
55510
  }), method(_void(), ServerUpdateActionResultSchema, {
55312
55511
  kind: "mutation",
55313
55512
  auth: "admin"
55513
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
55514
+ kind: "mutation",
55515
+ auth: "admin"
55314
55516
  });
55315
55517
  /**
55316
55518
  * `smtp-provider` — pluggable email delivery surface.
@@ -60219,14 +60421,13 @@ reset: method(object({
60219
60421
  auth: "admin"
60220
60422
  }) },
60221
60423
  /**
60222
- * A composed consumable (item added to a device with no native consumables
60223
- * provider) refuses `reset` by name: "Only meaningful when the item's
60224
- * `resettable` is true" — a composed item is never resettable until
60225
- * commands (`forward`) arrive in slice 3 (D663).
60424
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
60425
+ * name: only a composed device of a NEW type forwards an item's reset to a
60426
+ * source button, and marks that item `resettable` (D967).
60226
60427
  */
60227
60428
  composedMethods: { reset: {
60228
60429
  kind: "refuse",
60229
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
60430
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
60230
60431
  } },
60231
60432
  status: {
60232
60433
  schema: ConsumablesStatusSchema,
@@ -60820,7 +61021,7 @@ var DishwasherStatusSchema = object({
60820
61021
  /** Ms epoch of the last value received from the appliance; 0 = never. */
60821
61022
  lastFetchedAt: number()
60822
61023
  });
60823
- var DeviceIdSchema = number().int().nonnegative();
61024
+ var DeviceIdSchema$1 = number().int().nonnegative();
60824
61025
  /** A value a program option or setting can take. */
60825
61026
  var DishwasherValueSchema = union([
60826
61027
  boolean(),
@@ -60895,77 +61096,77 @@ var dishwasherCapability = {
60895
61096
  * option feature keys; each is checked against the program's constraints.
60896
61097
  */
60897
61098
  start: method(object({
60898
- deviceId: DeviceIdSchema,
61099
+ deviceId: DeviceIdSchema$1,
60899
61100
  program: string().min(1).optional(),
60900
61101
  options: record(string(), DishwasherValueSchema).optional()
60901
61102
  }), _void(), {
60902
61103
  kind: "mutation",
60903
61104
  auth: "protected"
60904
61105
  }),
60905
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
61106
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60906
61107
  kind: "mutation",
60907
61108
  auth: "protected"
60908
61109
  }),
60909
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
61110
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60910
61111
  kind: "mutation",
60911
61112
  auth: "protected"
60912
61113
  }),
60913
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
61114
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60914
61115
  kind: "mutation",
60915
61116
  auth: "protected"
60916
61117
  }),
60917
61118
  /** Select (not start) a program; refused when the appliance does not offer it. */
60918
61119
  setProgram: method(object({
60919
- deviceId: DeviceIdSchema,
61120
+ deviceId: DeviceIdSchema$1,
60920
61121
  program: string().min(1)
60921
61122
  }), _void(), {
60922
61123
  kind: "mutation",
60923
61124
  auth: "protected"
60924
61125
  }),
60925
61126
  setPower: method(object({
60926
- deviceId: DeviceIdSchema,
61127
+ deviceId: DeviceIdSchema$1,
60927
61128
  on: boolean()
60928
61129
  }), _void(), {
60929
61130
  kind: "mutation",
60930
61131
  auth: "protected"
60931
61132
  }),
60932
61133
  setHalfLoad: method(object({
60933
- deviceId: DeviceIdSchema,
61134
+ deviceId: DeviceIdSchema$1,
60934
61135
  on: boolean()
60935
61136
  }), _void(), {
60936
61137
  kind: "mutation",
60937
61138
  auth: "protected"
60938
61139
  }),
60939
61140
  setHygienePlus: method(object({
60940
- deviceId: DeviceIdSchema,
61141
+ deviceId: DeviceIdSchema$1,
60941
61142
  on: boolean()
60942
61143
  }), _void(), {
60943
61144
  kind: "mutation",
60944
61145
  auth: "protected"
60945
61146
  }),
60946
61147
  setExtraDry: method(object({
60947
- deviceId: DeviceIdSchema,
61148
+ deviceId: DeviceIdSchema$1,
60948
61149
  on: boolean()
60949
61150
  }), _void(), {
60950
61151
  kind: "mutation",
60951
61152
  auth: "protected"
60952
61153
  }),
60953
61154
  setIntensiveZone: method(object({
60954
- deviceId: DeviceIdSchema,
61155
+ deviceId: DeviceIdSchema$1,
60955
61156
  on: boolean()
60956
61157
  }), _void(), {
60957
61158
  kind: "mutation",
60958
61159
  auth: "protected"
60959
61160
  }),
60960
61161
  setVarioSpeedPlus: method(object({
60961
- deviceId: DeviceIdSchema,
61162
+ deviceId: DeviceIdSchema$1,
60962
61163
  on: boolean()
60963
61164
  }), _void(), {
60964
61165
  kind: "mutation",
60965
61166
  auth: "protected"
60966
61167
  }),
60967
61168
  setChildLock: method(object({
60968
- deviceId: DeviceIdSchema,
61169
+ deviceId: DeviceIdSchema$1,
60969
61170
  on: boolean()
60970
61171
  }), _void(), {
60971
61172
  kind: "mutation",
@@ -60977,7 +61178,7 @@ var dishwasherCapability = {
60977
61178
  * its declared range / members.
60978
61179
  */
60979
61180
  setOption: method(object({
60980
- deviceId: DeviceIdSchema,
61181
+ deviceId: DeviceIdSchema$1,
60981
61182
  key: string().min(1),
60982
61183
  value: DishwasherValueSchema
60983
61184
  }), _void(), {
@@ -60985,7 +61186,7 @@ var dishwasherCapability = {
60985
61186
  auth: "protected"
60986
61187
  }),
60987
61188
  /** What this appliance can do, from its own description — the UI is derived from it. */
60988
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
61189
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
60989
61190
  },
60990
61191
  status: {
60991
61192
  schema: DishwasherStatusSchema,
@@ -64676,7 +64877,8 @@ var TopologyNodeSchema = object({
64676
64877
  isHub: boolean(),
64677
64878
  isOnline: boolean(),
64678
64879
  cpuPercent: number(),
64679
- memoryPercent: number(),
64880
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
64881
+ memoryPercent: number().nullable(),
64680
64882
  uptime: number(),
64681
64883
  lastSeen: string(),
64682
64884
  localIps: array(string()).readonly(),
@@ -65294,9 +65496,10 @@ var PetFeederStatusSchema = object({
65294
65496
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
65295
65497
  * the device has not reported yet. */
65296
65498
  foodLevel: number().nullable().meta(readingMeta("none")),
65297
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
65298
- * feeder's own units. Null when the model reports none. */
65299
- bowlFood: number().nullable().meta(readingMeta("none")),
65499
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
65500
+ * feeder with a bowl scale; null when the model has none or its reading is
65501
+ * of an unproven unit. */
65502
+ bowlFood: number().nullable().meta(readingMeta("mass")),
65300
65503
  /** True while the pet is eating (`state.eating`). */
65301
65504
  eating: boolean(),
65302
65505
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -65480,6 +65683,137 @@ var petFeederCapability = {
65480
65683
  }
65481
65684
  } }
65482
65685
  };
65686
+ /**
65687
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
65688
+ * circulating drinking fountain: how much water is in the tank, how much the
65689
+ * pet drank, whether the pump runs and on what duty cycle.
65690
+ *
65691
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
65692
+ * a container of loose entities (a pump switch, two number boxes, a "filter
65693
+ * changed" button), and an operator BUILDS the device from them — a composed
65694
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
65695
+ * method of ONE source entity, and every reading is a field derived from a
65696
+ * source entity; the provider-shaped interface below exists so the composed
65697
+ * device and any future native provider answer the same contract.
65698
+ *
65699
+ * Quantities are stored in the base unit of their kind (D944): volumes in
65700
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
65701
+ * reports millilitres or minutes is converted in the composition's expression,
65702
+ * never here. Every reading may be `null`: a source that is offline or does
65703
+ * not report is unknown, never zero.
65704
+ *
65705
+ * The filter is not a field of this cap: it is an item of the `consumables`
65706
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
65707
+ */
65708
+ var PetFountainStatusSchema = object({
65709
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
65710
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65711
+ /** Water in the tank, in litres. Null while the source has not reported. */
65712
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
65713
+ /** True when the tank is empty. Null while unknown. */
65714
+ empty: boolean().nullable(),
65715
+ /** True while the pump is running. Null while unknown. */
65716
+ pumpRunning: boolean().nullable(),
65717
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
65718
+ pumpIntervalEnabled: boolean().nullable(),
65719
+ /** How long the pump runs per cycle, in seconds. */
65720
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
65721
+ /** How long the pump rests per cycle, in seconds. */
65722
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
65723
+ /** Water drunk in the last drink, in litres. */
65724
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
65725
+ /** Water drunk in the current hour, in litres. */
65726
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
65727
+ /** Water drunk over the last 24 hours (rolling), in litres. */
65728
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
65729
+ /** Water drunk since local midnight, in litres. */
65730
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
65731
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
65732
+ error: string().nullable(),
65733
+ /** Ms epoch when the slice was last refreshed. */
65734
+ lastFetchedAt: number()
65735
+ });
65736
+ var DeviceIdSchema = number().int().nonnegative();
65737
+ var MinutesSchema = number().int().min(0);
65738
+ var petFountainCapability = {
65739
+ name: "pet-fountain",
65740
+ scope: "device",
65741
+ deviceNative: true,
65742
+ mode: "singleton",
65743
+ deviceTypes: [DeviceType.PetFountain],
65744
+ methods: {
65745
+ /** Start or stop the pump. */
65746
+ setPump: method(object({
65747
+ deviceId: DeviceIdSchema,
65748
+ on: boolean()
65749
+ }), _void(), {
65750
+ kind: "mutation",
65751
+ auth: "protected"
65752
+ }),
65753
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
65754
+ setPumpInterval: method(object({
65755
+ deviceId: DeviceIdSchema,
65756
+ on: boolean()
65757
+ }), _void(), {
65758
+ kind: "mutation",
65759
+ auth: "protected"
65760
+ }),
65761
+ /** Set how many minutes the pump runs per cycle. */
65762
+ setPumpOnMinutes: method(object({
65763
+ deviceId: DeviceIdSchema,
65764
+ minutes: MinutesSchema
65765
+ }), _void(), {
65766
+ kind: "mutation",
65767
+ auth: "protected"
65768
+ }),
65769
+ /** Set how many minutes the pump rests per cycle. */
65770
+ setPumpOffMinutes: method(object({
65771
+ deviceId: DeviceIdSchema,
65772
+ minutes: MinutesSchema
65773
+ }), _void(), {
65774
+ kind: "mutation",
65775
+ auth: "protected"
65776
+ }),
65777
+ /** Clear the rolling daily consumption counters on the device. */
65778
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
65779
+ kind: "mutation",
65780
+ auth: "protected"
65781
+ })
65782
+ },
65783
+ status: {
65784
+ schema: PetFountainStatusSchema,
65785
+ kind: "push"
65786
+ },
65787
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
65788
+ runtimeState: PetFountainStatusSchema,
65789
+ /**
65790
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
65791
+ *
65792
+ * See `RuntimeStateDurability`. Enforced by
65793
+ * `scripts/check-runtime-state-durability.ts`.
65794
+ */
65795
+ durability: "session",
65796
+ /** History (D850): recorded series. */
65797
+ history: { fields: {
65798
+ waterLevel: {
65799
+ kind: "gauge",
65800
+ unit: "%",
65801
+ deadband: 1
65802
+ },
65803
+ waterVolume: {
65804
+ kind: "gauge",
65805
+ unit: "L"
65806
+ },
65807
+ todayDrink: {
65808
+ kind: "gauge",
65809
+ unit: "L"
65810
+ },
65811
+ last24hDrink: {
65812
+ kind: "gauge",
65813
+ unit: "L"
65814
+ }
65815
+ } }
65816
+ };
65483
65817
  var VehicleSchema = object({
65484
65818
  id: string(),
65485
65819
  name: string(),
@@ -71402,7 +71736,7 @@ discriminatedUnion("kind", [object({
71402
71736
  })]);
71403
71737
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
71404
71738
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
71405
- 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.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;
71739
+ 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;
71406
71740
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
71407
71741
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
71408
71742
  new Set(CAP_NAMES);
@@ -71753,6 +72087,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
71753
72087
  notifier: notifierCapability,
71754
72088
  numericSensor: numericSensorCapability,
71755
72089
  petFeeder: petFeederCapability,
72090
+ petFountain: petFountainCapability,
71756
72091
  powerMeter: powerMeterCapability,
71757
72092
  presence: presenceCapability,
71758
72093
  pressureSensor: pressureSensorCapability,
@@ -73003,7 +73338,7 @@ var BaseDeviceProvider = class extends BaseAddon {
73003
73338
  return toDeviceSummary(device, this.addonId);
73004
73339
  }
73005
73340
  };
73006
- 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;
73341
+ 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;
73007
73342
  new Set(Object.values(DeviceType));
73008
73343
  Object.freeze({
73009
73344
  "accessories.getStatus": {
@@ -74938,6 +75273,13 @@ Object.freeze({
74938
75273
  access: "create",
74939
75274
  role: "admin"
74940
75275
  },
75276
+ "deviceExport.exposeEntity": {
75277
+ capName: "device-export",
75278
+ capScope: "system",
75279
+ addonId: null,
75280
+ access: "create",
75281
+ role: "admin"
75282
+ },
74941
75283
  "deviceExport.getDeviceLiveContribution": {
74942
75284
  capName: "device-export",
74943
75285
  capScope: "system",
@@ -74966,6 +75308,13 @@ Object.freeze({
74966
75308
  access: "view",
74967
75309
  role: "admin"
74968
75310
  },
75311
+ "deviceExport.listExposedEntities": {
75312
+ capName: "device-export",
75313
+ capScope: "system",
75314
+ addonId: null,
75315
+ access: "view",
75316
+ role: "admin"
75317
+ },
74969
75318
  "deviceExport.listSupportedDeviceKinds": {
74970
75319
  capName: "device-export",
74971
75320
  capScope: "system",
@@ -74973,6 +75322,13 @@ Object.freeze({
74973
75322
  access: "view",
74974
75323
  role: "admin"
74975
75324
  },
75325
+ "deviceExport.listSupportedEntityKinds": {
75326
+ capName: "device-export",
75327
+ capScope: "system",
75328
+ addonId: null,
75329
+ access: "view",
75330
+ role: "admin"
75331
+ },
74976
75332
  "deviceExport.unexposeDevice": {
74977
75333
  capName: "device-export",
74978
75334
  capScope: "system",
@@ -74980,6 +75336,13 @@ Object.freeze({
74980
75336
  access: "create",
74981
75337
  role: "admin"
74982
75338
  },
75339
+ "deviceExport.unexposeEntity": {
75340
+ capName: "device-export",
75341
+ capScope: "system",
75342
+ addonId: null,
75343
+ access: "create",
75344
+ role: "admin"
75345
+ },
74983
75346
  "deviceExtension.listCandidates": {
74984
75347
  capName: "device-extension",
74985
75348
  capScope: "device",
@@ -78445,6 +78808,48 @@ Object.freeze({
78445
78808
  access: "create",
78446
78809
  role: "viewer"
78447
78810
  },
78811
+ "petFountain.getStatus": {
78812
+ capName: "pet-fountain",
78813
+ capScope: "device",
78814
+ addonId: null,
78815
+ access: "view",
78816
+ role: "viewer"
78817
+ },
78818
+ "petFountain.resetDailyConsumption": {
78819
+ capName: "pet-fountain",
78820
+ capScope: "device",
78821
+ addonId: null,
78822
+ access: "delete",
78823
+ role: "viewer"
78824
+ },
78825
+ "petFountain.setPump": {
78826
+ capName: "pet-fountain",
78827
+ capScope: "device",
78828
+ addonId: null,
78829
+ access: "create",
78830
+ role: "viewer"
78831
+ },
78832
+ "petFountain.setPumpInterval": {
78833
+ capName: "pet-fountain",
78834
+ capScope: "device",
78835
+ addonId: null,
78836
+ access: "create",
78837
+ role: "viewer"
78838
+ },
78839
+ "petFountain.setPumpOffMinutes": {
78840
+ capName: "pet-fountain",
78841
+ capScope: "device",
78842
+ addonId: null,
78843
+ access: "create",
78844
+ role: "viewer"
78845
+ },
78846
+ "petFountain.setPumpOnMinutes": {
78847
+ capName: "pet-fountain",
78848
+ capScope: "device",
78849
+ addonId: null,
78850
+ access: "create",
78851
+ role: "viewer"
78852
+ },
78448
78853
  "pipelineAnalytics.addRetrainFrameFromRecording": {
78449
78854
  capName: "pipeline-analytics",
78450
78855
  capScope: "device",
@@ -80846,6 +81251,13 @@ Object.freeze({
80846
81251
  access: "create",
80847
81252
  role: "admin"
80848
81253
  },
81254
+ "serverManagement.getLaunchOverrides": {
81255
+ capName: "server-management",
81256
+ capScope: "system",
81257
+ addonId: null,
81258
+ access: "view",
81259
+ role: "admin"
81260
+ },
80849
81261
  "serverManagement.getServerPackageStatus": {
80850
81262
  capName: "server-management",
80851
81263
  capScope: "system",
@@ -80853,6 +81265,13 @@ Object.freeze({
80853
81265
  access: "view",
80854
81266
  role: "admin"
80855
81267
  },
81268
+ "serverManagement.installDesktopUpdate": {
81269
+ capName: "server-management",
81270
+ capScope: "system",
81271
+ addonId: null,
81272
+ access: "create",
81273
+ role: "admin"
81274
+ },
80856
81275
  "serverManagement.restartServer": {
80857
81276
  capName: "server-management",
80858
81277
  capScope: "system",
@@ -80867,6 +81286,13 @@ Object.freeze({
80867
81286
  access: "create",
80868
81287
  role: "admin"
80869
81288
  },
81289
+ "serverManagement.setLaunchOverrides": {
81290
+ capName: "server-management",
81291
+ capScope: "system",
81292
+ addonId: null,
81293
+ access: "create",
81294
+ role: "admin"
81295
+ },
80870
81296
  "settingsStore.aggregate": {
80871
81297
  capName: "settings-store",
80872
81298
  capScope: "system",
@@ -84927,6 +85353,100 @@ var APPLIANCE_CAP_VOCABULARY = {
84927
85353
  }
84928
85354
  ]
84929
85355
  },
85356
+ "pet-fountain": {
85357
+ scope: "device",
85358
+ noun: t("Pet fountain", "Fontana per animali"),
85359
+ actions: [
85360
+ {
85361
+ id: "petFountain.pumpOn",
85362
+ method: "setPump",
85363
+ label: t("Pump on", "Pompa accesa"),
85364
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
85365
+ icon: "play",
85366
+ args: { on: true }
85367
+ },
85368
+ {
85369
+ id: "petFountain.pumpOff",
85370
+ method: "setPump",
85371
+ label: t("Pump off", "Pompa spenta"),
85372
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
85373
+ icon: "square",
85374
+ args: { on: false }
85375
+ },
85376
+ {
85377
+ id: "petFountain.intervalOn",
85378
+ method: "setPumpInterval",
85379
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
85380
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
85381
+ icon: "timer",
85382
+ args: { on: true }
85383
+ },
85384
+ {
85385
+ id: "petFountain.intervalOff",
85386
+ method: "setPumpInterval",
85387
+ label: t("Pump cycle off", "Ciclo pompa spento"),
85388
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
85389
+ icon: "timer-off",
85390
+ args: { on: false }
85391
+ },
85392
+ {
85393
+ id: "petFountain.pumpOnMinutes",
85394
+ method: "setPumpOnMinutes",
85395
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
85396
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
85397
+ icon: "timer",
85398
+ openArgs: [{
85399
+ key: "minutes",
85400
+ label: t("Minutes", "Minuti"),
85401
+ offer: {
85402
+ kind: "number",
85403
+ min: 0,
85404
+ step: 1,
85405
+ unit: "min"
85406
+ }
85407
+ }]
85408
+ },
85409
+ {
85410
+ id: "petFountain.pumpOffMinutes",
85411
+ method: "setPumpOffMinutes",
85412
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
85413
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
85414
+ icon: "timer-off",
85415
+ openArgs: [{
85416
+ key: "minutes",
85417
+ label: t("Minutes", "Minuti"),
85418
+ offer: {
85419
+ kind: "number",
85420
+ min: 0,
85421
+ step: 1,
85422
+ unit: "min"
85423
+ }
85424
+ }]
85425
+ },
85426
+ {
85427
+ id: "petFountain.resetDailyConsumption",
85428
+ method: "resetDailyConsumption",
85429
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
85430
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
85431
+ icon: "rotate-ccw"
85432
+ }
85433
+ ],
85434
+ state: [{
85435
+ kind: "boolean",
85436
+ field: "empty",
85437
+ label: t("Tank empty", "Serbatoio vuoto"),
85438
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
85439
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
85440
+ icon: "droplet"
85441
+ }, {
85442
+ kind: "numeric",
85443
+ field: "waterLevel",
85444
+ quantity: t("the water level", "il livello dell’acqua"),
85445
+ label: t("Water level", "Livello dell’acqua"),
85446
+ unit: "%",
85447
+ icon: "droplets"
85448
+ }]
85449
+ },
84930
85450
  "media-player": {
84931
85451
  scope: "device",
84932
85452
  noun: t("Media player", "Lettore multimediale"),
@@ -86864,7 +87384,7 @@ function projectAction(cap, action) {
86864
87384
  };
86865
87385
  }
86866
87386
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
86867
- 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;
87387
+ 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;
86868
87388
  DeviceFeature.BatteryOperated;
86869
87389
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
86870
87390
  var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
@@ -88415,6 +88935,36 @@ Object.freeze({
88415
88935
  form: "single",
88416
88936
  optional: false
88417
88937
  }],
88938
+ "petFountain.getStatus": [{
88939
+ name: "deviceId",
88940
+ form: "single",
88941
+ optional: false
88942
+ }],
88943
+ "petFountain.resetDailyConsumption": [{
88944
+ name: "deviceId",
88945
+ form: "single",
88946
+ optional: false
88947
+ }],
88948
+ "petFountain.setPump": [{
88949
+ name: "deviceId",
88950
+ form: "single",
88951
+ optional: false
88952
+ }],
88953
+ "petFountain.setPumpInterval": [{
88954
+ name: "deviceId",
88955
+ form: "single",
88956
+ optional: false
88957
+ }],
88958
+ "petFountain.setPumpOffMinutes": [{
88959
+ name: "deviceId",
88960
+ form: "single",
88961
+ optional: false
88962
+ }],
88963
+ "petFountain.setPumpOnMinutes": [{
88964
+ name: "deviceId",
88965
+ form: "single",
88966
+ optional: false
88967
+ }],
88418
88968
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
88419
88969
  name: "deviceId",
88420
88970
  form: "single",
@@ -133568,6 +134118,38 @@ function alexaRegionOptions(amazonDomain) {
133568
134118
  };
133569
134119
  }
133570
134120
  //#endregion
134121
+ //#region src/alexa-url-params.ts
134122
+ var BASE = "http://placeholder.invalid";
134123
+ function describeUrlParams(url) {
134124
+ let parsed;
134125
+ try {
134126
+ parsed = new URL(url, BASE);
134127
+ } catch {
134128
+ return {
134129
+ path: "",
134130
+ names: [],
134131
+ oa2: false,
134132
+ returnTo: false,
134133
+ assocHandle: false,
134134
+ mode: false
134135
+ };
134136
+ }
134137
+ const names = [...new Set(parsed.searchParams.keys())].sort();
134138
+ return {
134139
+ path: parsed.pathname,
134140
+ names,
134141
+ oa2: names.some((n) => n.startsWith("openid.oa2.")),
134142
+ returnTo: names.includes("openid.return_to"),
134143
+ assocHandle: names.includes("openid.assoc_handle"),
134144
+ mode: names.includes("openid.mode")
134145
+ };
134146
+ }
134147
+ /** One-line, value-free rendering of {@link describeUrlParams}. */
134148
+ function formatUrlParams(url) {
134149
+ const d = describeUrlParams(url);
134150
+ return `names=[${d.names.join(",")}] oa2=${d.oa2} return_to=${d.returnTo} assoc_handle=${d.assocHandle} mode=${d.mode}`;
134151
+ }
134152
+ //#endregion
133571
134153
  //#region src/alexa-proxy-trace.ts
133572
134154
  /**
133573
134155
  * Sanitised one-line trace of the alexa-cookie2 proxy's own log lines.
@@ -133578,7 +134160,9 @@ function alexaRegionOptions(amazonDomain) {
133578
134160
  * redirect target — always WITHOUT a query string or fragment. Any line that is
133579
134161
  * not one of the known step lines yields `null` and is never traced.
133580
134162
  */
133581
- var ROUTER_RE = /^Router: (\S+) \/ ([A-Z]+) \//;
134163
+ var ROUTER_RE = /^Router: (\S+) \/ ([A-Z]+) \/ (.*)$/;
134164
+ var INITIAL_REQUEST_RE = /^Alexa-Cookie: Initial Page Request: (\S+)$/;
134165
+ var USER_AGENT_RE = /"user-agent":"([^"]*)"/i;
133582
134166
  var PROXY_REQUEST_RE = /^Alexa-Cookie: Proxy-Request: ([A-Z]+) (\S+)$/;
133583
134167
  var PROXY_RESPONSE_RE = /^Alexa-Cookie: Proxy Response from Host: (\S+)$/;
133584
134168
  var REDIRECT_ORIGINAL_RE = /^Redirect: Original Location ----> (\S+)$/;
@@ -133588,17 +134172,32 @@ function stripQuery(target) {
133588
134172
  const cut = target.search(/[?#]/);
133589
134173
  return cut === -1 ? target : target.slice(0, cut);
133590
134174
  }
134175
+ /** The target without its query, plus the value-free parameter summary. */
134176
+ function withParams(target) {
134177
+ return `${stripQuery(target)} ${formatUrlParams(target)}`;
134178
+ }
134179
+ /** Browser family of the header block's user-agent; the string itself is never kept. */
134180
+ function userAgentFamily(headers) {
134181
+ const ua = USER_AGENT_RE.exec(headers)?.[1];
134182
+ if (ua === void 0) return "none";
134183
+ if (/CriOS|Chrome|Chromium|Edg\//.test(ua)) return "chrome";
134184
+ if (/Firefox|FxiOS/.test(ua)) return "firefox";
134185
+ if (/Safari/.test(ua)) return "safari";
134186
+ return "other";
134187
+ }
133591
134188
  function sanitizeProxyTraceLine(line) {
133592
134189
  const router = ROUTER_RE.exec(line);
133593
- if (router !== null) return `router ${router[2]} ${stripQuery(router[1] ?? "")}`;
134190
+ if (router !== null) return `router ${router[2]} ${withParams(router[1] ?? "")} ua=${userAgentFamily(router[3] ?? "")}`;
134191
+ const initial = INITIAL_REQUEST_RE.exec(line);
134192
+ if (initial !== null) return `initial-request ${withParams(initial[1] ?? "")}`;
133594
134193
  const request = PROXY_REQUEST_RE.exec(line);
133595
- if (request !== null) return `proxy-request ${request[1]} ${stripQuery(request[2] ?? "")}`;
134194
+ if (request !== null) return `proxy-request ${request[1]} ${withParams(request[2] ?? "")}`;
133596
134195
  const response = PROXY_RESPONSE_RE.exec(line);
133597
134196
  if (response !== null) return `proxy-response host=${response[1]}`;
133598
134197
  const original = REDIRECT_ORIGINAL_RE.exec(line);
133599
- if (original !== null) return `redirect-original ${stripQuery(original[1] ?? "")}`;
134198
+ if (original !== null) return `redirect-original ${withParams(original[1] ?? "")}`;
133600
134199
  const final = REDIRECT_FINAL_RE.exec(line);
133601
- if (final !== null) return `redirect-final ${stripQuery(final[1] ?? "")}`;
134200
+ if (final !== null) return `redirect-final ${withParams(final[1] ?? "")}`;
133602
134201
  return null;
133603
134202
  }
133604
134203
  //#endregion
@@ -133714,7 +134313,7 @@ var AlexaProxyLoginSession = class {
133714
134313
  const line = args.map(String).join(" ");
133715
134314
  const trace = sanitizeProxyTraceLine(line);
133716
134315
  if (trace !== null) {
133717
- this.#logger.info(`alexa proxy-login: ${trace}`, { meta: { sessionId: this.#opts.sessionId } });
134316
+ this.#logger.info(`[alexa-login-trace] ${trace}`, { meta: { sessionId: this.#opts.sessionId } });
133718
134317
  return;
133719
134318
  }
133720
134319
  if (SECRET_LINE_RE.test(line)) return;
@@ -133892,6 +134491,13 @@ var AlexaProxyLoginManager = class {
133892
134491
  this.#getLoginProxy = deps.getLoginProxy;
133893
134492
  this.#captureWaitMs = deps.captureWaitMs ?? CAPTURE_WAIT_MS;
133894
134493
  }
134494
+ /** One value-free line per upstream hop the hub login proxy sees (names only). */
134495
+ #traceHop(sessionId, view) {
134496
+ const url = describeUrlParams(view.url);
134497
+ const landing = url.path.endsWith("/ap/maplanding") ? " maplanding" : "";
134498
+ const location = view.location === null ? "none" : `${describeUrlParams(view.location).path} ${formatUrlParams(view.location)}`;
134499
+ this.#logger.info(`[alexa-login-trace] hub-hop${landing} status=${view.status} url=${url.path} ${formatUrlParams(view.url)} location=${location}`, { meta: { sessionId } });
134500
+ }
133895
134501
  /**
133896
134502
  * Start a fresh proxy-login. Cancels any in-flight session first (single
133897
134503
  * fixed port). Returns the `loginUrl` for the onboarding UI to open.
@@ -133910,7 +134516,10 @@ var AlexaProxyLoginManager = class {
133910
134516
  host: upstreamHost,
133911
134517
  scheme: "http"
133912
134518
  }],
133913
- terminal: cookieSuccessTerminal,
134519
+ terminal: (view) => {
134520
+ this.#traceHop(sessionId, view);
134521
+ return cookieSuccessTerminal(view);
134522
+ },
133914
134523
  onDone: async (end) => {
133915
134524
  if (end === "no_authorization_code") {
133916
134525
  this.#logger.warn("alexa proxy-login: Amazon ended the login without an authorization code", { meta: { sessionId } });