@camstack/types 1.2.442 → 1.2.444

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/addon.d.ts +5 -1
  3. package/dist/addon.js +21 -5
  4. package/dist/addon.mjs +17 -6
  5. package/dist/automation/export-entities.d.ts +98 -0
  6. package/dist/capabilities/capability-definition.d.ts +1 -1
  7. package/dist/capabilities/composer.cap.d.ts +34 -0
  8. package/dist/capabilities/consumables.cap.d.ts +4 -5
  9. package/dist/capabilities/core-blocks.cap.d.ts +244 -0
  10. package/dist/capabilities/device-export.cap.d.ts +111 -1
  11. package/dist/capabilities/index.d.ts +8 -3
  12. package/dist/capabilities/metrics-provider.cap.d.ts +16 -16
  13. package/dist/capabilities/node-removal-hook.cap.d.ts +67 -0
  14. package/dist/capabilities/nodes.cap.d.ts +2 -2
  15. package/dist/capabilities/pet-fountain.cap.d.ts +138 -0
  16. package/dist/capabilities/pipeline-executor.cap.d.ts +6 -22
  17. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +3 -4
  18. package/dist/capabilities/pipeline-runner.cap.d.ts +2 -2
  19. package/dist/capabilities/pipeline-step-input.d.ts +33 -0
  20. package/dist/client.js +817 -798
  21. package/dist/client.mjs +817 -798
  22. package/dist/composition/composition-items.d.ts +3 -3
  23. package/dist/composition/composition-report.d.ts +17 -0
  24. package/dist/composition/composition.d.ts +68 -1
  25. package/dist/composition/index.d.ts +2 -0
  26. package/dist/composition/presets/pet-fountain.d.ts +36 -0
  27. package/dist/composition/validate-forward.d.ts +31 -0
  28. package/dist/{composition-C0dlyG9U.js → composition-CO6Sak-s.js} +49 -5
  29. package/dist/{composition-bIBW5_13.mjs → composition-kSuOifuG.mjs} +32 -6
  30. package/dist/device/cap-vocabulary/entries/appliances.d.ts +1 -1
  31. package/dist/device/device-control-resolution.d.ts +1 -1
  32. package/dist/device/device-type.d.ts +4 -0
  33. package/dist/{err-msg-BCdlR2Li.mjs → err-msg-DWXWc0Di.mjs} +4 -0
  34. package/dist/{err-msg-COHgq7Mb.js → err-msg-EJhsFHqa.js} +4 -0
  35. package/dist/generated/addon-api.d.ts +149 -32
  36. package/dist/generated/addon-cap-facts.d.ts +1 -1
  37. package/dist/generated/cap-status-types.d.ts +3 -1
  38. package/dist/generated/capability-router-map.d.ts +10 -4
  39. package/dist/generated/collection-array-methods.d.ts +2 -2
  40. package/dist/generated/customize-catalog.d.ts +1 -1
  41. package/dist/generated/device-local-state.d.ts +3 -0
  42. package/dist/generated/device-proxy.d.ts +3 -0
  43. package/dist/generated/device-scoped-caps.d.ts +1 -1
  44. package/dist/generated/history-catalog.d.ts +1 -1
  45. package/dist/generated/method-access-map.d.ts +1 -1
  46. package/dist/generated/method-device-selectors.d.ts +1 -1
  47. package/dist/generated/runtime-state-durability.d.ts +1 -1
  48. package/dist/generated/system-proxy.d.ts +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1616 -607
  51. package/dist/index.mjs +1581 -596
  52. package/dist/interfaces/agent.d.ts +6 -2
  53. package/dist/interfaces/event-bus.d.ts +1 -1
  54. package/dist/node.js +2 -2
  55. package/dist/node.mjs +2 -2
  56. package/dist/{sleep-DJws6ho7.mjs → sleep-6eG9C8Xl.mjs} +224 -72
  57. package/dist/{sleep-BpwNJ1Su.js → sleep-UXDHn-oJ.js} +247 -71
  58. package/dist/types/agent-pipeline-settings.d.ts +2 -2
  59. package/dist/utils/oauth-landing.d.ts +1 -1
  60. package/dist/vendor-login-proxy/flow-trace.d.ts +29 -0
  61. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-BpwNJ1Su.js");
2
+ const require_sleep = require("./sleep-UXDHn-oJ.js");
3
3
  const require_event_category = require("./event-category-E17zaHjN.js");
4
- const require_composition = require("./composition-C0dlyG9U.js");
4
+ const require_composition = require("./composition-CO6Sak-s.js");
5
5
  const require_node_color = require("./node-color.js");
6
- const require_err_msg = require("./err-msg-COHgq7Mb.js");
6
+ const require_err_msg = require("./err-msg-EJhsFHqa.js");
7
7
  const require_enums = require("./enums.js");
8
8
  let zod = require("zod");
9
9
  //#region src/media/media-owner-key.ts
@@ -10705,6 +10705,593 @@ function buildActionSteps(steps, vars, deps) {
10705
10705
  });
10706
10706
  }
10707
10707
  //#endregion
10708
+ //#region src/capabilities/notification-output.cap.ts
10709
+ /**
10710
+ * notification-output — canonical, capability-gated notification delivery.
10711
+ *
10712
+ * Apprise-derived model (see
10713
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
10714
+ * callers emit ONE canonical `Notification`; each provider declares a
10715
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
10716
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
10717
+ * message to what the kind supports — callers never special-case a service.
10718
+ *
10719
+ * DESIGN DECISIONS (locked):
10720
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
10721
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
10722
+ * cap. Rationale: the admin UI needs one uniform surface across the
10723
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
10724
+ * alternative would fork the UI per addon and cannot host the
10725
+ * discovery→adopt flow.
10726
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
10727
+ * the generated cap-mount auto-`concatCollection`-fans them across every
10728
+ * registered provider (notifiers addon + HA addon) so one catalog is
10729
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
10730
+ * `addonId` the generated collection router extracts from the call input.
10731
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
10732
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
10733
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
10734
+ * base64 fallback needed.
10735
+ *
10736
+ * TODO (deferred, closed-set change — separate decision): add
10737
+ * `providerKind: 'notify'` so notification providers surface on the unified
10738
+ * admin "Integrations" page.
10739
+ */
10740
+ /**
10741
+ * Zentik-derived typed-media enum — the superset across every kind. Each
10742
+ * adapter picks what it supports and the degrade engine filters the rest.
10743
+ */
10744
+ var AttachmentMediaTypeSchema = zod.z.enum([
10745
+ "image",
10746
+ "video",
10747
+ "gif",
10748
+ "audio",
10749
+ "icon"
10750
+ ]);
10751
+ /**
10752
+ * A single attachment. Exactly one of `url` (remote source, most adapters
10753
+ * prefer this) or `bytes` (inline source; required for Pushover-style
10754
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
10755
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
10756
+ */
10757
+ var AttachmentSchema = zod.z.object({
10758
+ mediaType: AttachmentMediaTypeSchema,
10759
+ url: zod.z.string().optional(),
10760
+ bytes: zod.z.instanceof(Uint8Array).optional(),
10761
+ mime: zod.z.string().optional(),
10762
+ name: zod.z.string().optional()
10763
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
10764
+ var NotificationFormatSchema = zod.z.enum([
10765
+ "text",
10766
+ "markdown",
10767
+ "html"
10768
+ ]);
10769
+ /**
10770
+ * The CLOSED icon vocabulary an action button may use.
10771
+ *
10772
+ * A closed set, not a free string, and that is the whole point: an arbitrary
10773
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
10774
+ * notices — the same class of gap as a zone vocabulary nothing produced
10775
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
10776
+ * declares `actionIcons: false` and the degrade engine strips the field.
10777
+ *
10778
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
10779
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
10780
+ * differently.
10781
+ *
10782
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
10783
+ *
10784
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
10785
+ * the whole set onto its own renderer's vocabulary through a
10786
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
10787
+ * fallback, so adding a member here fails every adapter's build until someone
10788
+ * decides its glyph, which is the only place that decision can be made
10789
+ * honestly.
10790
+ *
10791
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
10792
+ * `disarm` straight through; iOS feeds that string to
10793
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
10794
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
10795
+ * mapping, and "the field is documented" is not "the value renders".
10796
+ *
10797
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
10798
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
10799
+ * so an addon that emits a token the running hub does not know does not lose an
10800
+ * icon — its whole `send` fails Zod validation and the notification never
10801
+ * arrives. Never emit a new token from an addon before the train carrying it.
10802
+ */
10803
+ var NotificationActionIconSchema = zod.z.enum([
10804
+ "acknowledge",
10805
+ "dismiss",
10806
+ "silence",
10807
+ "snooze",
10808
+ "view",
10809
+ "play",
10810
+ "open",
10811
+ "close",
10812
+ "lock",
10813
+ "unlock",
10814
+ "arm",
10815
+ "arm-home",
10816
+ "arm-away",
10817
+ "arm-night",
10818
+ "disarm",
10819
+ "light",
10820
+ "alert",
10821
+ "camera"
10822
+ ]);
10823
+ /** A single tap-through action button. */
10824
+ var NotificationActionSchema = zod.z.object({
10825
+ id: zod.z.string(),
10826
+ label: zod.z.string(),
10827
+ url: zod.z.string().optional(),
10828
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
10829
+ icon: NotificationActionIconSchema.optional(),
10830
+ /**
10831
+ * Renders in a warning style where the notifier supports it.
10832
+ *
10833
+ * A HINT, never a gate. The callback's authority is its token and nothing
10834
+ * else — see `notification-center/action-token.ts` for what that does and
10835
+ * does not buy.
10836
+ */
10837
+ destructive: zod.z.boolean().optional(),
10838
+ /**
10839
+ * How the tap should REACH the url.
10840
+ *
10841
+ * `navigate` (absent, and every button authored before this field) opens it:
10842
+ * the phone leaves the notification and shows whatever the callback returns.
10843
+ * That is right for a button whose answer the operator wants to read.
10844
+ *
10845
+ * `background` fires it as a POST and stays put. It exists for the buttons
10846
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
10847
+ * an answer to the notification, and being thrown into a browser tab to
10848
+ * confirm it costs more attention than the notification did. A backend that
10849
+ * cannot do a background call renders it as an ordinary link (the adapters
10850
+ * fall back rather than dropping the button), so this is a preference, never
10851
+ * a requirement.
10852
+ */
10853
+ mode: zod.z.enum(["navigate", "background"]).optional()
10854
+ });
10855
+ /** A delivery parameter value: what a notifier declared it takes. */
10856
+ var DeliveryParamValueSchema = zod.z.union([
10857
+ zod.z.string(),
10858
+ zod.z.number(),
10859
+ zod.z.boolean()
10860
+ ]);
10861
+ /**
10862
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
10863
+ * addresses a target of that kind may set it; a kind that declares nothing is
10864
+ * never handed one. The admin UI derives its control from this descriptor —
10865
+ * no editor knows a service by name.
10866
+ *
10867
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
10868
+ * run on the addon cap path).
10869
+ */
10870
+ var DeliveryParamSchema = zod.z.object({
10871
+ key: zod.z.string().min(1),
10872
+ label: zod.z.string(),
10873
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
10874
+ labelKey: zod.z.string().optional(),
10875
+ type: zod.z.enum([
10876
+ "enum",
10877
+ "number",
10878
+ "boolean",
10879
+ "string"
10880
+ ]),
10881
+ /** `enum` only: the closed set of accepted values. */
10882
+ options: zod.z.array(zod.z.object({
10883
+ value: zod.z.string(),
10884
+ label: zod.z.string()
10885
+ })).optional(),
10886
+ /** `number` only. */
10887
+ min: zod.z.number().optional(),
10888
+ max: zod.z.number().optional(),
10889
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
10890
+ default: DeliveryParamValueSchema.optional()
10891
+ });
10892
+ /**
10893
+ * The canonical notification. `body` is the only hard field (Apprise model).
10894
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
10895
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
10896
+ * the adapter maps this ordinal onto its native level. `level?` is an
10897
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
10898
+ * `priority` for that one target.
10899
+ */
10900
+ var NotificationSchema = zod.z.object({
10901
+ body: zod.z.string(),
10902
+ title: zod.z.string().optional(),
10903
+ format: NotificationFormatSchema.default("text"),
10904
+ priority: zod.z.number().int().min(1).max(5).default(3),
10905
+ level: zod.z.string().optional(),
10906
+ attachments: zod.z.array(AttachmentSchema).optional(),
10907
+ clickUrl: zod.z.string().optional(),
10908
+ actions: zod.z.array(NotificationActionSchema).optional(),
10909
+ sound: zod.z.string().optional(),
10910
+ ttl: zod.z.number().optional(),
10911
+ tag: zod.z.string().optional(),
10912
+ deviceId: zod.z.number().optional(),
10913
+ eventId: zod.z.string().optional(),
10914
+ metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
10915
+ /**
10916
+ * Values for the parameters the target's kind DECLARES in
10917
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
10918
+ * declaration by the sender; a notifier reads only the keys it declared.
10919
+ */
10920
+ delivery: zod.z.record(zod.z.string(), DeliveryParamValueSchema).optional()
10921
+ });
10922
+ /** One declared native severity/priority level for a kind. */
10923
+ var TargetKindLevelSchema = zod.z.object({
10924
+ id: zod.z.string(),
10925
+ label: zod.z.string(),
10926
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
10927
+ ordinal: zod.z.number().int().min(1).max(5).nullable(),
10928
+ flags: zod.z.object({
10929
+ critical: zod.z.boolean().optional(),
10930
+ silent: zod.z.boolean().optional(),
10931
+ noPush: zod.z.boolean().optional()
10932
+ }).optional(),
10933
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
10934
+ requires: zod.z.array(zod.z.string()).optional(),
10935
+ description: zod.z.string().optional()
10936
+ });
10937
+ /** Attachment capabilities for a kind (drives the degrade engine + test panel). */
10938
+ var TargetKindAttachmentsCapsSchema = zod.z.object({
10939
+ mediaTypes: zod.z.array(AttachmentMediaTypeSchema),
10940
+ mode: zod.z.enum([
10941
+ "url",
10942
+ "bytes",
10943
+ "both"
10944
+ ]),
10945
+ max: zod.z.number().int().nonnegative(),
10946
+ maxBytes: zod.z.number().int().positive().optional()
10947
+ });
10948
+ /** The full capability block consulted before dispatch. */
10949
+ var TargetKindCapsSchema = zod.z.object({
10950
+ attachments: TargetKindAttachmentsCapsSchema,
10951
+ /** Max action buttons (0 = none). */
10952
+ actions: zod.z.number().int().nonnegative(),
10953
+ /**
10954
+ * Whether this kind renders a per-action ICON.
10955
+ *
10956
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
10957
+ * run on the addon cap path — three production failures in one day taught
10958
+ * this repo that once. Absent is read as false by the degrade engine, which
10959
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
10960
+ * assumed and dropped costs the operator's trust in the field.
10961
+ */
10962
+ actionIcons: zod.z.boolean().optional(),
10963
+ levels: zod.z.array(TargetKindLevelSchema),
10964
+ format: zod.z.array(NotificationFormatSchema),
10965
+ clickUrl: zod.z.boolean(),
10966
+ sound: zod.z.boolean(),
10967
+ ttl: zod.z.boolean(),
10968
+ bodyMaxLen: zod.z.number().int().positive()
10969
+ });
10970
+ /**
10971
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
10972
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
10973
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
10974
+ * the union is large and not meant for runtime validation here; the exported
10975
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
10976
+ */
10977
+ var ConfigSchemaPassthrough$3 = zod.z.unknown();
10978
+ var TargetKindSchema = zod.z.object({
10979
+ kind: zod.z.string(),
10980
+ label: zod.z.string(),
10981
+ icon: zod.z.string(),
10982
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
10983
+ addonId: zod.z.string(),
10984
+ /**
10985
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
10986
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
10987
+ * when the addon bundles no icon for that kind — the client then falls back
10988
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
10989
+ *
10990
+ * Root-relative on purpose: it resolves against whatever origin serves a web
10991
+ * client, and a native client joins it onto its own hub base.
10992
+ *
10993
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
10994
+ * field that survived only because the runtime cap-router forwards provider
10995
+ * output verbatim — so every consumer had to re-declare it by hand to stop
10996
+ * its own Zod parse from stripping it, and the whole arrangement would have
10997
+ * broken silently the moment output validation was tightened anywhere.
10998
+ */
10999
+ iconUrl: zod.z.string().optional(),
11000
+ /**
11001
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
11002
+ *
11003
+ * The server knows this and therefore says it, because the client cannot
11004
+ * safely guess: a React-Native client renders SVG and raster through two
11005
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
11006
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
11007
+ * placeholder glyph for every vector icon while the web build looked fine.
11008
+ *
11009
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
11010
+ * not been updated — a client that cannot determine the type should prefer
11011
+ * its raster path, which is the safe default for an unknown image.
11012
+ */
11013
+ iconMediaType: zod.z.string().optional(),
11014
+ configSchema: ConfigSchemaPassthrough$3,
11015
+ /**
11016
+ * The config keys that decide WHERE this kind's secrets are sent (server,
11017
+ * base URL, instance, chat, recipient). A save that changes any of them
11018
+ * while leaving a secret redacted is refused by the provider (D634), so an
11019
+ * editor uses this to ask for the secret on exactly those edits instead of
11020
+ * on every edit.
11021
+ *
11022
+ * Optional: absent from a provider that holds no secret (the Home Assistant
11023
+ * kinds) and from a provider older than this field — an editor that finds
11024
+ * it absent must assume ANY edit may need the secret.
11025
+ */
11026
+ destinationKeys: zod.z.array(zod.z.string()).optional(),
11027
+ supportsDiscovery: zod.z.boolean(),
11028
+ /**
11029
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
11030
+ * Absent = none; the rule editor then offers nothing.
11031
+ */
11032
+ deliveryParams: zod.z.array(DeliveryParamSchema).optional(),
11033
+ caps: TargetKindCapsSchema
11034
+ });
11035
+ /**
11036
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
11037
+ * (return a presence marker only) when serving `listTargets` — never
11038
+ * round-trip a stored secret to the UI.
11039
+ */
11040
+ var TargetSchema = zod.z.object({
11041
+ id: zod.z.string(),
11042
+ name: zod.z.string(),
11043
+ kind: zod.z.string(),
11044
+ addonId: zod.z.string(),
11045
+ enabled: zod.z.boolean(),
11046
+ config: zod.z.record(zod.z.string(), zod.z.unknown())
11047
+ });
11048
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
11049
+ var DiscoveredTargetSchema = zod.z.object({
11050
+ kind: zod.z.string(),
11051
+ suggestedName: zod.z.string(),
11052
+ config: zod.z.record(zod.z.string(), zod.z.unknown())
11053
+ });
11054
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
11055
+ var RenderedAsSchema = zod.z.object({
11056
+ level: zod.z.string(),
11057
+ format: NotificationFormatSchema,
11058
+ attachmentsSent: zod.z.number().int().nonnegative(),
11059
+ actionsSent: zod.z.number().int().nonnegative(),
11060
+ truncated: zod.z.boolean(),
11061
+ dropped: zod.z.array(zod.z.string())
11062
+ });
11063
+ var SendResultSchema = zod.z.object({
11064
+ success: zod.z.boolean(),
11065
+ error: zod.z.string().optional(),
11066
+ renderedAs: RenderedAsSchema.optional()
11067
+ });
11068
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
11069
+ var TestResultSchema = SendResultSchema;
11070
+ /**
11071
+ * Pre-save credential probe against a partial (unsaved) target config.
11072
+ * `resolvedUrl` never includes secrets (token stripped from path).
11073
+ */
11074
+ var ProbeTargetConfigResultSchema = zod.z.object({
11075
+ ok: zod.z.boolean(),
11076
+ resolvedUrl: zod.z.string(),
11077
+ httpStatus: zod.z.number().int().optional(),
11078
+ stateInstance: zod.z.string().optional(),
11079
+ error: zod.z.string().optional()
11080
+ });
11081
+ var notificationOutputCapability = {
11082
+ name: "notification-output",
11083
+ scope: "system",
11084
+ mode: "collection",
11085
+ methods: {
11086
+ listTargetKinds: require_sleep.method(zod.z.object({}), zod.z.array(TargetKindSchema)),
11087
+ listTargets: require_sleep.method(zod.z.object({}), zod.z.array(TargetSchema), { auth: "admin" }),
11088
+ /**
11089
+ * One target WITH secrets — admin edit / duplicate hydration only.
11090
+ * `listTargets` stays redacted; the editor loads this so password fields
11091
+ * are filled (masked in the input) and a save can round-trip the token
11092
+ * when a destination changes (Duplicate with a new chatId, etc.).
11093
+ */
11094
+ getTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), TargetSchema, { auth: "admin" }),
11095
+ discoverTargets: require_sleep.method(zod.z.object({
11096
+ kind: zod.z.string(),
11097
+ config: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
11098
+ /**
11099
+ * When editing / duplicating, the blank password field never round-trips
11100
+ * the stored token. Pass the source target id so the provider can restore
11101
+ * secrets server-side before dialling (same merge as upsert).
11102
+ */
11103
+ targetId: zod.z.string().optional()
11104
+ }), zod.z.array(DiscoveredTargetSchema), { auth: "admin" }),
11105
+ probeTargetConfig: require_sleep.method(zod.z.object({
11106
+ kind: zod.z.string(),
11107
+ config: zod.z.record(zod.z.string(), zod.z.unknown()),
11108
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
11109
+ targetId: zod.z.string().optional()
11110
+ }), ProbeTargetConfigResultSchema, {
11111
+ kind: "mutation",
11112
+ auth: "admin"
11113
+ }),
11114
+ send: require_sleep.method(zod.z.object({
11115
+ targetId: zod.z.string(),
11116
+ notification: NotificationSchema
11117
+ }), SendResultSchema, {
11118
+ kind: "mutation",
11119
+ auth: "admin"
11120
+ }),
11121
+ testTarget: require_sleep.method(zod.z.object({
11122
+ targetId: zod.z.string(),
11123
+ sample: NotificationSchema.optional()
11124
+ }), TestResultSchema, {
11125
+ kind: "mutation",
11126
+ auth: "admin"
11127
+ }),
11128
+ upsertTarget: require_sleep.method(zod.z.object({
11129
+ target: TargetSchema,
11130
+ /**
11131
+ * Duplicate flow: create a NEW target id while copying secrets from an
11132
+ * existing row. The UI never holds the stored token; blank / redacted
11133
+ * password fields are restored from this source (D634 still applies if
11134
+ * destinations diverge without a re-typed secret).
11135
+ */
11136
+ cloneSecretsFrom: zod.z.string().optional()
11137
+ }), TargetSchema, {
11138
+ kind: "mutation",
11139
+ auth: "admin"
11140
+ }),
11141
+ deleteTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), zod.z.void(), {
11142
+ kind: "mutation",
11143
+ auth: "admin"
11144
+ }),
11145
+ setTargetEnabled: require_sleep.method(zod.z.object({
11146
+ targetId: zod.z.string(),
11147
+ enabled: zod.z.boolean()
11148
+ }), zod.z.void(), {
11149
+ kind: "mutation",
11150
+ auth: "admin"
11151
+ })
11152
+ }
11153
+ };
11154
+ //#endregion
11155
+ //#region src/automation/export-entities.ts
11156
+ function errorText(err) {
11157
+ return err instanceof Error ? err.message : String(err);
11158
+ }
11159
+ /** `actions.run` flattens a refusal to `[code] text`; the code leads. */
11160
+ function refusalFromThrown(message) {
11161
+ if (message.startsWith("[busy]")) return "already-running";
11162
+ if (message.startsWith("[not-found]")) return "not-found";
11163
+ return null;
11164
+ }
11165
+ async function runExportedAction(api, log, input) {
11166
+ const { exporter, actionId } = input;
11167
+ const tags = { actionId };
11168
+ const refuse = (reason, detail) => {
11169
+ log.warn(`${exporter}: exported action not run — ${reason}`, {
11170
+ tags,
11171
+ meta: {
11172
+ reason,
11173
+ ...detail ? { detail } : {}
11174
+ }
11175
+ });
11176
+ return {
11177
+ status: "refused",
11178
+ reason
11179
+ };
11180
+ };
11181
+ try {
11182
+ const result = await api.actions.run.mutate({ actionId });
11183
+ if (result.outcome === "skipped-overlap") return refuse("already-running", result.message);
11184
+ if (result.outcome === "skipped-cooldown") return refuse("cooldown", result.message);
11185
+ if (result.outcome === "failed" || result.outcome === "aborted") {
11186
+ const message = result.message ?? result.outcome;
11187
+ log.warn(`${exporter}: exported action failed`, {
11188
+ tags,
11189
+ meta: {
11190
+ outcome: result.outcome,
11191
+ message
11192
+ }
11193
+ });
11194
+ return {
11195
+ status: "failed",
11196
+ message
11197
+ };
11198
+ }
11199
+ log.info(`${exporter}: exported action ran`, {
11200
+ tags,
11201
+ meta: { outcome: result.outcome }
11202
+ });
11203
+ return { status: "started" };
11204
+ } catch (err) {
11205
+ const message = errorText(err);
11206
+ const reason = refusalFromThrown(message);
11207
+ if (reason !== null) return refuse(reason, message);
11208
+ log.warn(`${exporter}: exported action failed`, {
11209
+ tags,
11210
+ meta: { message }
11211
+ });
11212
+ return {
11213
+ status: "failed",
11214
+ message
11215
+ };
11216
+ }
11217
+ }
11218
+ /** Send through one CamStack notification target on behalf of an exporter. */
11219
+ async function sendToExportedTarget(api, log, input) {
11220
+ const { exporter, targetId } = input;
11221
+ const tags = { targetId };
11222
+ try {
11223
+ const target = (await api.notificationOutput.listTargets.query({})).find((t) => t.id === targetId);
11224
+ if (target === void 0) {
11225
+ log.warn(`${exporter}: exported notifier not used — target-gone`, {
11226
+ tags,
11227
+ meta: { reason: "target-gone" }
11228
+ });
11229
+ return {
11230
+ status: "refused",
11231
+ reason: "target-gone"
11232
+ };
11233
+ }
11234
+ if (!target.enabled) {
11235
+ log.warn(`${exporter}: exported notifier not used — target-disabled`, {
11236
+ tags,
11237
+ meta: { reason: "target-disabled" }
11238
+ });
11239
+ return {
11240
+ status: "refused",
11241
+ reason: "target-disabled"
11242
+ };
11243
+ }
11244
+ const result = await api.notificationOutput.send.mutate({
11245
+ targetId,
11246
+ addonId: target.addonId,
11247
+ notification: NotificationSchema.parse(input.notification)
11248
+ });
11249
+ if (!result.success) {
11250
+ const message = result.error ?? "send failed";
11251
+ log.warn(`${exporter}: exported notifier send failed`, {
11252
+ tags,
11253
+ meta: { message }
11254
+ });
11255
+ return {
11256
+ status: "failed",
11257
+ message
11258
+ };
11259
+ }
11260
+ return { status: "sent" };
11261
+ } catch (err) {
11262
+ const message = errorText(err);
11263
+ log.warn(`${exporter}: exported notifier send failed`, {
11264
+ tags,
11265
+ meta: { message }
11266
+ });
11267
+ return {
11268
+ status: "failed",
11269
+ message
11270
+ };
11271
+ }
11272
+ }
11273
+ /**
11274
+ * Split an exporter's selection into what still exists and what was deleted.
11275
+ * Devices are never judged here (their lifecycle is the tombstone, D865).
11276
+ * Callers pass ids from a query that ANSWERED — an unanswered list must not
11277
+ * reach this function (D49: a failed read changes nothing).
11278
+ */
11279
+ function pruneDeletedEntities(selection, live, log, exporter) {
11280
+ const kept = [];
11281
+ const dropped = [];
11282
+ for (const ref of selection) if (ref.kind === "action" && !live.actionIds.has(ref.actionId)) {
11283
+ dropped.push(ref);
11284
+ log.warn(`${exporter}: exported action was deleted — dropped from the export`, { tags: { actionId: ref.actionId } });
11285
+ } else if (ref.kind === "notifier" && !live.targetIds.has(ref.targetId)) {
11286
+ dropped.push(ref);
11287
+ log.warn(`${exporter}: exported notifier target was deleted — dropped from the export`, { tags: { targetId: ref.targetId } });
11288
+ } else kept.push(ref);
11289
+ return {
11290
+ kept,
11291
+ dropped
11292
+ };
11293
+ }
11294
+ //#endregion
10708
11295
  //#region src/capabilities/addon-pages.cap.ts
10709
11296
  /**
10710
11297
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
@@ -10744,71 +11331,6 @@ var addonPagesCapability = {
10744
11331
  methods: { listPages: require_sleep.method(zod.z.void(), zod.z.array(AddonPageInfoSchema).readonly()) }
10745
11332
  };
10746
11333
  //#endregion
10747
- //#region src/capabilities/addon-pages-source.cap.ts
10748
- /**
10749
- * `addon-pages-source` — collection cap exposing per-provider raw page
10750
- * declarations. Every addon that contributes a UI page registers a
10751
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
10752
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
10753
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
10754
- * that admin-ui consumes.
10755
- *
10756
- * The split exists because the public listing has a different output
10757
- * shape than the per-provider raw declarations, and we want both ends
10758
- * to flow through codegen instead of relying on a hand-written wrapper.
10759
- */
10760
- var AddonPageDeclarationSchema = zod.z.object({
10761
- id: zod.z.string(),
10762
- label: zod.z.string(),
10763
- icon: zod.z.string(),
10764
- path: zod.z.string(),
10765
- /**
10766
- * Module Federation remote name — must match the `name` field on the
10767
- * page addon's `federation()` plugin config. Used by admin-ui's
10768
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
10769
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
10770
- * contain hyphens).
10771
- */
10772
- remoteName: zod.z.string(),
10773
- /**
10774
- * Bundle filename inside the addon's `dist/` dir served at
10775
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
10776
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
10777
- * the static-file route can compute an mtime-based cache-buster URL
10778
- * without a separate filesystem stat.
10779
- */
10780
- bundle: zod.z.string(),
10781
- /**
10782
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
10783
- * `'cluster'`, `'administration'` — the page renders inside that group.
10784
- * Any OTHER string creates (or joins) a custom section rendered after
10785
- * the built-in groups; its label comes from `sectionLabel` (first
10786
- * declaration wins), falling back to the id. Absent → the legacy
10787
- * "Addon Pages" group.
10788
- */
10789
- section: zod.z.string().optional(),
10790
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
10791
- sectionLabel: zod.z.string().optional()
10792
- });
10793
- var addonPagesSourceCapability = {
10794
- name: "addon-pages-source",
10795
- scope: "system",
10796
- mode: "collection",
10797
- internal: true,
10798
- methods: {
10799
- /**
10800
- * `internal: true` did not gate the mount (see the 2026-08-26 note on
10801
- * `data-store-provider`) — this was reachable on the AppRouter by ANY
10802
- * authenticated session at the default `auth: 'protected'`. The only
10803
- * consumer, the `addon-pages-aggregator` builtin, reads it via
10804
- * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
10805
- * only, never tRPC (per `ctx.capabilities` seeing only the local
10806
- * process). The public, enriched listing admin-ui actually consumes is
10807
- * the separate `addon-pages` cap, unaffected by this change.
10808
- */
10809
- listPages: require_sleep.method(zod.z.void(), zod.z.array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
10810
- };
10811
- //#endregion
10812
11334
  //#region src/capabilities/addon-routes.cap.ts
10813
11335
  var AddonHttpRouteSchema = zod.z.object({
10814
11336
  method: zod.z.enum([
@@ -14915,7 +15437,11 @@ var CompositionProblemCodeSchema = zod.z.enum([
14915
15437
  "mixed-quantity",
14916
15438
  "helper-mismatch",
14917
15439
  "member-constraint",
14918
- "retention"
15440
+ "retention",
15441
+ "command-method-unknown",
15442
+ "command-source-unknown",
15443
+ "command-input-unknown",
15444
+ "command-input-missing"
14919
15445
  ]);
14920
15446
  /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.temperature`. */
14921
15447
  var CompositionProblemSchema = zod.z.object({
@@ -14957,7 +15483,9 @@ var CompositionFieldRoleSchema = zod.z.enum([
14957
15483
  var CompositionFieldItemSchema = zod.z.object({
14958
15484
  arrayPath: zod.z.string(),
14959
15485
  key: zod.z.string(),
14960
- label: zod.z.string()
15486
+ label: zod.z.string(),
15487
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
15488
+ resettable: zod.z.boolean().optional()
14961
15489
  });
14962
15490
  var CompositionFieldPlanSchema = zod.z.object({
14963
15491
  cap: zod.z.string(),
@@ -15478,7 +16006,7 @@ function unavailableFor(tf, role, mode, cap, at) {
15478
16006
  }
15479
16007
  //#endregion
15480
16008
  //#region src/composition/composition-items.ts
15481
- /** Item fields the composer writes itself, with the value it writes. */
16009
+ /** Item fields the composer writes itself, with the value it writes. `resettable` is the default: an item with a `reset` forward writes `true` (D967). */
15482
16010
  var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
15483
16011
  function reservedItemFields(itemArray) {
15484
16012
  return new Set([
@@ -15521,7 +16049,8 @@ function planItemField(ctx, leaf) {
15521
16049
  item: {
15522
16050
  arrayPath: itemArray.path,
15523
16051
  key,
15524
- label
16052
+ label,
16053
+ ...leaf.resettable ? { resettable: true } : {}
15525
16054
  },
15526
16055
  featureIndex,
15527
16056
  kind: tf.kind,
@@ -15561,6 +16090,7 @@ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
15561
16090
  }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
15562
16091
  key,
15563
16092
  label: item.label,
16093
+ resettable: item.reset !== void 0,
15564
16094
  path,
15565
16095
  source
15566
16096
  }))]);
@@ -15572,12 +16102,163 @@ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
15572
16102
  };
15573
16103
  }
15574
16104
  //#endregion
16105
+ //#region src/composition/validate-forward.ts
16106
+ /**
16107
+ * Save-time validation of `forward` commands (D659, amended by D967).
16108
+ *
16109
+ * A BUILT capability serves a method only through a command, so every method
16110
+ * the cap serves needs one. A forward is checked against the two input schemas
16111
+ * it joins: the BUILT method's (what the caller sends) and the SOURCE method's
16112
+ * (what is called). An input field in neither place is refused BY NAME, and so
16113
+ * is a required source input nothing provides. `deviceId` is the source's own
16114
+ * and is never an argument.
16115
+ *
16116
+ * Static: it needs only the capability definitions. That the source DEVICE
16117
+ * exists and binds the cap is `forwardSourceProblems`, with the sources read.
16118
+ */
16119
+ var DEVICE_ID_FIELD = "deviceId";
16120
+ /** Every forward of a feature: the method commands, then each item's `reset`. */
16121
+ function featureForwards(feature, featureIndex) {
16122
+ const at = `features[${featureIndex}]`;
16123
+ const commands = Object.entries(feature.commands ?? {}).flatMap(([method, target]) => target.kind === "forward" ? [{
16124
+ at: `${at}.commands.${method}`,
16125
+ method,
16126
+ command: target
16127
+ }] : []);
16128
+ const resets = Object.entries(feature.items ?? {}).flatMap(([key, item]) => item.reset === void 0 ? [] : [{
16129
+ at: `${at}.items.${key}.reset`,
16130
+ method: "reset",
16131
+ itemKey: key,
16132
+ command: item.reset
16133
+ }]);
16134
+ return [...commands, ...resets];
16135
+ }
16136
+ /** Every source a forward of an enabled feature calls, once. */
16137
+ function forwardSourceRefs(composition) {
16138
+ const refs = composition.features.flatMap((feature, index) => feature.kind === "fields" && require_composition.compositionFeatureEnabled(feature) ? featureForwards(feature, index).map((forward) => forward.command.source) : []);
16139
+ return [...new Map(refs.map((ref) => [require_composition.compositionSourceKey(ref), ref])).values()];
16140
+ }
16141
+ /** The methods a BUILT cap must give a command: served, and not answered by the composer itself. */
16142
+ function commandedMethods(def) {
16143
+ return Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => name);
16144
+ }
16145
+ var NO_INPUT = {
16146
+ all: /* @__PURE__ */ new Set(),
16147
+ required: /* @__PURE__ */ new Set()
16148
+ };
16149
+ /** The top-level input fields of a method, `deviceId` removed; null when the input is not an object. */
16150
+ function inputFields(def, method) {
16151
+ const input = def.methods[method]?.input;
16152
+ if (input === void 0) return null;
16153
+ const root = unwrapSchema(input);
16154
+ if (!(root instanceof zod.z.ZodObject)) return null;
16155
+ const entries = Object.entries(root.shape).filter(([name]) => name !== DEVICE_ID_FIELD);
16156
+ return {
16157
+ all: new Set(entries.map(([name]) => name)),
16158
+ required: new Set(entries.flatMap(([name, field]) => field instanceof zod.z.ZodType && !field.safeParse(void 0).success ? [name] : []))
16159
+ };
16160
+ }
16161
+ function inputRefs(arg) {
16162
+ if (typeof arg !== "object") return [];
16163
+ if (require_composition.isForwardInputRef(arg)) return [arg.$input];
16164
+ return Object.values(arg).flatMap(inputRefs);
16165
+ }
16166
+ function names(list) {
16167
+ return [...list].map((name) => `\`${name}\``).join(", ");
16168
+ }
16169
+ function sourceMethodOf(forward, lookupCap) {
16170
+ const { command, at } = forward;
16171
+ const def = lookupCap(command.cap);
16172
+ if (def === null) return {
16173
+ def: null,
16174
+ problems: [problem("command-source-unknown", `${at}.cap`, `no capability is named \`${command.cap}\``)]
16175
+ };
16176
+ const served = def.methods[command.method];
16177
+ if (served === void 0 || served.systemOnly === true) return {
16178
+ def: null,
16179
+ problems: [problem("command-source-unknown", `${at}.method`, `\`${command.cap}\` has no method \`${command.method}\` to forward to`)]
16180
+ };
16181
+ if (served.kind !== "mutation") return {
16182
+ def: null,
16183
+ problems: [problem("command-source-unknown", `${at}.method`, `\`${command.cap}.${command.method}\` is a query — a command calls a mutation of the source`)]
16184
+ };
16185
+ return {
16186
+ def,
16187
+ problems: []
16188
+ };
16189
+ }
16190
+ function mappedInputProblems(forward, built, source, args) {
16191
+ const { at, command } = forward;
16192
+ const where = `${command.cap}.${command.method}`;
16193
+ const problems = [];
16194
+ if ("deviceId" in args) problems.push(problem("command-input-unknown", `${at}.args.${DEVICE_ID_FIELD}`, `\`${DEVICE_ID_FIELD}\` is the source device's own — a forward never takes it as an argument`));
16195
+ if (source !== null) {
16196
+ for (const key of Object.keys(args).filter((k) => k !== "deviceId" && !source.all.has(k))) problems.push(problem("command-input-unknown", `${at}.args.${key}`, `\`${where}\` has no input field \`${key}\` (it takes ${names(source.all)})`));
16197
+ const missing = [...source.required].filter((k) => !(k in args));
16198
+ if (missing.length > 0) problems.push(problem("command-input-missing", `${at}.args`, `\`${where}\` needs ${names(missing)}, which \`args\` does not provide`));
16199
+ }
16200
+ const readable = forward.itemKey === void 0 ? built : NO_INPUT;
16201
+ if (readable !== null) for (const ref of Object.values(args).flatMap(inputRefs)) {
16202
+ if (readable.all.has(ref)) continue;
16203
+ problems.push(problem("command-input-unknown", `${at}.args`, readable === NO_INPUT ? `an item's \`reset\` takes no input — \`$input: ${ref}\` has nothing to read` : `\`$input: ${ref}\` is not a field of the \`${forward.method}\` input (it takes ${names(readable.all)})`));
16204
+ }
16205
+ return problems;
16206
+ }
16207
+ function passthroughInputProblems(forward, built, source) {
16208
+ const { at, command } = forward;
16209
+ const where = `${command.cap}.${command.method}`;
16210
+ const sent = forward.itemKey === void 0 ? built : NO_INPUT;
16211
+ if (sent === null || source === null) return [];
16212
+ const problems = [];
16213
+ for (const field of [...sent.all].filter((f) => !source.all.has(f))) problems.push(problem("command-input-unknown", `${at}.args`, `\`${forward.method}\` takes \`${field}\`, which \`${where}\` does not (it takes ${names(source.all)}) — add \`args\` to map it`));
16214
+ const missing = [...source.required].filter((k) => !sent.all.has(k));
16215
+ if (missing.length > 0) problems.push(problem("command-input-missing", `${at}.args`, `\`${where}\` needs ${names(missing)}, which \`${forward.method}\` does not take — add \`args\` to provide it`));
16216
+ return problems;
16217
+ }
16218
+ /** One forward against the built cap's method and the source cap's method. */
16219
+ function forwardProblems(built, forward, lookupCap) {
16220
+ const source = sourceMethodOf(forward, lookupCap);
16221
+ if (source.def === null) return source.problems;
16222
+ const builtFields = inputFields(built, forward.method);
16223
+ const sourceFields = inputFields(source.def, forward.command.method);
16224
+ const mapping = forward.command.args;
16225
+ return mapping === void 0 ? passthroughInputProblems(forward, builtFields, sourceFields) : mappedInputProblems(forward, builtFields, sourceFields, mapping);
16226
+ }
16227
+ /** A command must name a method the built cap serves and the composer does not answer itself. */
16228
+ function commandMethodProblems(built, feature, at) {
16229
+ const commanded = new Set(commandedMethods(built));
16230
+ return Object.keys(feature.commands ?? {}).flatMap((method) => {
16231
+ if (commanded.has(method)) return [];
16232
+ const answered = built.composedMethods?.[method] !== void 0;
16233
+ return [problem("command-method-unknown", `${at}.commands.${method}`, answered ? `\`${built.name}.${method}\` is answered by the composer itself — an item's \`reset\` serves it` : `\`${built.name}\` has no method \`${method}\` to serve`)];
16234
+ });
16235
+ }
16236
+ /**
16237
+ * The source DEVICES of the forwards, read: each must exist and bind the cap it
16238
+ * is called on. A forward onto an unread device is refused rather than stored
16239
+ * unchecked, like every other source (D391).
16240
+ */
16241
+ function forwardSourceProblems(composition, sources) {
16242
+ return composition.features.flatMap((feature, index) => {
16243
+ if (feature.kind !== "fields" || !require_composition.compositionFeatureEnabled(feature)) return [];
16244
+ return featureForwards(feature, index).flatMap((forward) => {
16245
+ const key = require_composition.compositionSourceKey(forward.command.source);
16246
+ const desc = sources.get(key);
16247
+ const at = `${forward.at}.source`;
16248
+ if (desc === void 0 || desc.kind === "unreadable") return [problem("source-unreadable", at, `could not read source \`${key}\` (${desc === void 0 ? "it was not read" : desc.error}) — the save is refused rather than stored unchecked`)];
16249
+ if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
16250
+ if (!desc.caps.has(forward.command.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${forward.command.cap}\``)];
16251
+ return [];
16252
+ });
16253
+ });
16254
+ }
16255
+ //#endregion
15575
16256
  //#region src/composition/helpers/catalog.ts
15576
16257
  /**
15577
16258
  * One catalog row per base reading a group helper can build (D944 §3).
15578
16259
  * Data, not a form default written into the block.
15579
16260
  */
15580
- var NOW$2 = {
16261
+ var NOW$3 = {
15581
16262
  fieldPath: "lastFetchedAt",
15582
16263
  expr: "now"
15583
16264
  };
@@ -15629,7 +16310,7 @@ var GROUP_TEMPLATES = [
15629
16310
  quantity: "temperature",
15630
16311
  defaultAggregate: "mean",
15631
16312
  empty: "null",
15632
- companions: [NOW$2],
16313
+ companions: [NOW$3],
15633
16314
  alsoFrom: {
15634
16315
  deviceTypes: CLIMATE_DEVICES,
15635
16316
  accept: {
@@ -15647,7 +16328,7 @@ var GROUP_TEMPLATES = [
15647
16328
  quantity: "humidity",
15648
16329
  defaultAggregate: "mean",
15649
16330
  empty: "null",
15650
- companions: [NOW$2],
16331
+ companions: [NOW$3],
15651
16332
  alsoFrom: {
15652
16333
  deviceTypes: CLIMATE_DEVICES,
15653
16334
  accept: {
@@ -15665,7 +16346,7 @@ var GROUP_TEMPLATES = [
15665
16346
  quantity: "pressure",
15666
16347
  defaultAggregate: "mean",
15667
16348
  empty: "null",
15668
- companions: [NOW$2]
16349
+ companions: [NOW$3]
15669
16350
  }),
15670
16351
  numeric({
15671
16352
  id: "illuminance",
@@ -15676,7 +16357,7 @@ var GROUP_TEMPLATES = [
15676
16357
  quantity: "illuminance",
15677
16358
  defaultAggregate: "mean",
15678
16359
  empty: "null",
15679
- companions: [NOW$2]
16360
+ companions: [NOW$3]
15680
16361
  }),
15681
16362
  numeric({
15682
16363
  id: "power",
@@ -15687,7 +16368,7 @@ var GROUP_TEMPLATES = [
15687
16368
  quantity: "power",
15688
16369
  defaultAggregate: "sum",
15689
16370
  empty: "omit",
15690
- companions: [NOW$2]
16371
+ companions: [NOW$3]
15691
16372
  }),
15692
16373
  numeric({
15693
16374
  id: "energy",
@@ -15698,7 +16379,7 @@ var GROUP_TEMPLATES = [
15698
16379
  quantity: "energy",
15699
16380
  defaultAggregate: "sum",
15700
16381
  empty: "omit",
15701
- companions: [NOW$2]
16382
+ companions: [NOW$3]
15702
16383
  }),
15703
16384
  numeric({
15704
16385
  id: "battery",
@@ -15733,7 +16414,7 @@ var GROUP_TEMPLATES = [
15733
16414
  quantity: "device",
15734
16415
  defaultAggregate: "mean",
15735
16416
  empty: "null",
15736
- companions: [NOW$2]
16417
+ companions: [NOW$3]
15737
16418
  }),
15738
16419
  binary({
15739
16420
  id: "motion",
@@ -16159,7 +16840,7 @@ async function runFanOut(calls, invoke) {
16159
16840
  //#region src/generated/history-catalog.ts
16160
16841
  /**
16161
16842
  * Cap history catalog, projected from every `*.cap.ts` `history` block.
16162
- * 41 caps with series (76 series), 34 caps answered `'none'`.
16843
+ * 42 caps with series (80 series), 34 caps answered `'none'`.
16163
16844
  * Field keys are `runtimeState` paths (`a.b` into fixed objects only).
16164
16845
  */
16165
16846
  var HISTORY_CATALOG = {
@@ -16399,6 +17080,25 @@ var HISTORY_CATALOG = {
16399
17080
  unit: "s"
16400
17081
  }
16401
17082
  },
17083
+ "pet-fountain": {
17084
+ "waterLevel": {
17085
+ kind: "gauge",
17086
+ unit: "%",
17087
+ deadband: 1
17088
+ },
17089
+ "waterVolume": {
17090
+ kind: "gauge",
17091
+ unit: "L"
17092
+ },
17093
+ "todayDrink": {
17094
+ kind: "gauge",
17095
+ unit: "L"
17096
+ },
17097
+ "last24hDrink": {
17098
+ kind: "gauge",
17099
+ unit: "L"
17100
+ }
17101
+ },
16402
17102
  "power-meter": {
16403
17103
  "power": {
16404
17104
  kind: "gauge",
@@ -16596,7 +17296,7 @@ function historyFieldsFor(capName) {
16596
17296
  * Project a statistics or integral helper onto a new device (D944 §3).
16597
17297
  * The value field is `series`: the composer fills it from history.
16598
17298
  */
16599
- var NOW$1 = {
17299
+ var NOW$2 = {
16600
17300
  kind: "expression",
16601
17301
  expr: "now",
16602
17302
  bindings: {}
@@ -16656,7 +17356,7 @@ function draft(input) {
16656
17356
  cap: input.cap,
16657
17357
  fields: {
16658
17358
  [input.field]: series(input.member, "value", input.empty),
16659
- lastFetchedAt: NOW$1
17359
+ lastFetchedAt: NOW$2
16660
17360
  }
16661
17361
  }, {
16662
17362
  kind: "fields",
@@ -16738,12 +17438,12 @@ function integralDraft(spec, retention = require_composition.DEFAULT_HISTORY_RET
16738
17438
  * a free expression is the expression engine unchanged — it still includes a
16739
17439
  * stale input, and the template says so.
16740
17440
  */
16741
- var NOW = {
17441
+ var NOW$1 = {
16742
17442
  kind: "expression",
16743
17443
  expr: "now",
16744
17444
  bindings: {}
16745
17445
  };
16746
- function from$1(member) {
17446
+ function from$2(member) {
16747
17447
  return {
16748
17448
  kind: "from",
16749
17449
  source: member.source,
@@ -16774,7 +17474,7 @@ function numericSensor(helper, value, extra = []) {
16774
17474
  cap: "numeric-sensor",
16775
17475
  fields: {
16776
17476
  value,
16777
- lastFetchedAt: NOW
17477
+ lastFetchedAt: NOW$1
16778
17478
  }
16779
17479
  }, ...extra]
16780
17480
  };
@@ -16792,7 +17492,7 @@ function binarySensor(helper, on) {
16792
17492
  cap: "binary",
16793
17493
  fields: {
16794
17494
  on,
16795
- lastChangedAt: NOW
17495
+ lastChangedAt: NOW$1
16796
17496
  }
16797
17497
  }]
16798
17498
  };
@@ -16810,7 +17510,7 @@ function thresholdDraft(spec) {
16810
17510
  const delays = onFor !== null && offFor !== null && (spec.onForMs > 0 || spec.offForMs > 0) ? `, ${onFor}, ${offFor}` : "";
16811
17511
  return {
16812
17512
  problems,
16813
- draft: binarySensor(spec, expression$1(`coalesce(schmitt(x, ${high ?? "0"}, ${low ?? "0"}${delays}), false)`, { x: from$1(spec.source) }))
17513
+ draft: binarySensor(spec, expression$1(`coalesce(schmitt(x, ${high ?? "0"}, ${low ?? "0"}${delays}), false)`, { x: from$2(spec.source) }))
16814
17514
  };
16815
17515
  }
16816
17516
  var NAMES = ["a", "b"];
@@ -16854,7 +17554,7 @@ function arithmeticDraft(spec) {
16854
17554
  }])
16855
17555
  };
16856
17556
  }
16857
- const bindings = Object.fromEntries(spec.inputs.slice(0, 2).map((input, index) => [NAMES[index] ?? "a", from$1(input)]));
17557
+ const bindings = Object.fromEntries(spec.inputs.slice(0, 2).map((input, index) => [NAMES[index] ?? "a", from$2(input)]));
16858
17558
  return {
16859
17559
  problems,
16860
17560
  draft: numericSensor(spec, expression$1(spec.op === "difference" ? "a - b" : "b == 0 ? null : a / b", bindings))
@@ -16870,7 +17570,7 @@ function expressionProblems(spec) {
16870
17570
  seen.add(input.name);
16871
17571
  });
16872
17572
  if (problems.length > 0) return problems;
16873
- const bindings = Object.fromEntries(spec.inputs.map((input) => [input.name, from$1(input.member)]));
17573
+ const bindings = Object.fromEntries(spec.inputs.map((input) => [input.name, from$2(input.member)]));
16874
17574
  const message = require_composition.validateExpressionSource({
16875
17575
  expr: spec.expr,
16876
17576
  bindings
@@ -16879,7 +17579,7 @@ function expressionProblems(spec) {
16879
17579
  }
16880
17580
  function expressionDraft(spec) {
16881
17581
  const problems = expressionProblems(spec);
16882
- const bindings = Object.fromEntries(spec.inputs.map((input) => [input.name, from$1(input.member)]));
17582
+ const bindings = Object.fromEntries(spec.inputs.map((input) => [input.name, from$2(input.member)]));
16883
17583
  const source = expression$1(spec.expr, bindings);
16884
17584
  return {
16885
17585
  problems,
@@ -17396,8 +18096,9 @@ function actuatorCommandCaps(composition) {
17396
18096
  function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType, servedCommands) {
17397
18097
  const problems = [];
17398
18098
  if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) problems.push(problem("wrong-device-type", `${at}.cap`, `\`${def.name}\` is not offered on a \`${deviceType}\` device (it allows ${def.deviceTypes.join(", ")})`));
17399
- const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
17400
- if (served.length > 0 && !servedCommands.has(def.name)) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
18099
+ const commanded = new Set(Object.entries(feature.commands ?? {}).flatMap(([name, target]) => target.kind === "forward" ? [name] : []));
18100
+ const unserved = commandedMethods(def).filter((name) => !commanded.has(name)).map((name) => `\`${name}\``);
18101
+ if (unserved.length > 0 && !servedCommands.has(def.name)) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${unserved.join(", ")} with no command — a composed device serves a method only through a \`forward\` (D967)`));
17401
18102
  const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
17402
18103
  for (const key of requiredTopLevelKeys(state)) {
17403
18104
  if (key === itemArrayPath) continue;
@@ -17438,12 +18139,28 @@ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
17438
18139
  events: []
17439
18140
  };
17440
18141
  }
18142
+ /**
18143
+ * The commands of a feature (D967). Only a BUILT capability on a NEW device
18144
+ * serves a method through a `forward`; on an existing device the native keeps
18145
+ * its methods, and a `code` command is slice 4.
18146
+ */
18147
+ function commandProblems(def, feature, featureIndex, mode, lookupCap) {
18148
+ const at = `features[${featureIndex}]`;
18149
+ const forwards = featureForwards(feature, featureIndex);
18150
+ const problems = Object.entries(feature.commands ?? {}).filter(([, t]) => t.kind === "code").map(([method]) => problem("reserved-variant", `${at}.commands.${method}`, "a `code` command arrives in slice 4"));
18151
+ if (forwards.length > 0 && mode !== "new") return [...problems, problem("reserved-variant", `${at}.commands`, "commands (`forward`) are served only by a capability a NEW device builds — not on an existing device")];
18152
+ return [
18153
+ ...problems,
18154
+ ...commandMethodProblems(def, feature, at),
18155
+ ...forwards.flatMap((forward) => forwardProblems(def, forward, lookupCap))
18156
+ ];
18157
+ }
17441
18158
  function planFeature(feature, featureIndex, target, lookupCap, servedCommands) {
17442
18159
  const at = `features[${featureIndex}]`;
17443
18160
  const cap = composableCap(feature, at, lookupCap);
17444
18161
  if ("code" in cap) return refused(cap);
17445
18162
  const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType, servedCommands)];
17446
- if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
18163
+ capProblems.push(...commandProblems(cap.def, feature, featureIndex, target.mode, lookupCap));
17447
18164
  const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
17448
18165
  featureIndex,
17449
18166
  path,
@@ -17562,7 +18279,7 @@ function planComposition(composition, lookupCap, target) {
17562
18279
  }
17563
18280
  function compositionSourceRefs(composition) {
17564
18281
  const refs = composition.features.flatMap((f) => require_composition.compositionFeatureEnabled(f) && f.kind === "fields" ? require_composition.featureSources(f).flatMap(require_composition.fieldReads).map((r) => r.source) : []);
17565
- return [...new Map(refs.map((r) => [require_composition.compositionSourceKey(r), r])).values()];
18282
+ return [...new Map([...refs, ...forwardSourceRefs(composition)].map((r) => [require_composition.compositionSourceKey(r), r])).values()];
17566
18283
  }
17567
18284
  /**
17568
18285
  * The candidate's device: the existing target itself, else `self`, else a
@@ -17698,6 +18415,7 @@ function validateComposition(composition, ctx) {
17698
18415
  const problems = [
17699
18416
  ...plan.problems,
17700
18417
  ...sourceProblems(plan.fields, ctx, self),
18418
+ ...forwardSourceProblems(composition, ctx.sources),
17701
18419
  ...cycles,
17702
18420
  ...helperSaveProblems(composition, facts)
17703
18421
  ];
@@ -21058,6 +21776,36 @@ var ExposeInputSchema = zod.z.object({
21058
21776
  capabilities: zod.z.array(zod.z.string()).optional()
21059
21777
  });
21060
21778
  var UnexposeInputSchema = zod.z.object({ deviceId: zod.z.string() });
21779
+ /**
21780
+ * What an exporter may expose besides devices (D968). An action and a
21781
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
21782
+ * exporter's own selection, not the device registry.
21783
+ */
21784
+ var ExportEntityKindSchema = zod.z.enum([
21785
+ "device",
21786
+ "action",
21787
+ "notifier"
21788
+ ]);
21789
+ var ExportEntityRefSchema = zod.z.discriminatedUnion("kind", [
21790
+ zod.z.object({
21791
+ kind: zod.z.literal("device"),
21792
+ deviceId: zod.z.string()
21793
+ }),
21794
+ zod.z.object({
21795
+ kind: zod.z.literal("action"),
21796
+ actionId: zod.z.string().min(1)
21797
+ }),
21798
+ zod.z.object({
21799
+ kind: zod.z.literal("notifier"),
21800
+ targetId: zod.z.string().min(1)
21801
+ })
21802
+ ]);
21803
+ var ExposedEntitySchema = zod.z.object({
21804
+ entity: ExportEntityRefSchema,
21805
+ exposedAs: zod.z.string().optional()
21806
+ });
21807
+ var ExposeEntityInputSchema = zod.z.object({ entity: ExportEntityRefSchema });
21808
+ var UnexposeEntityInputSchema = zod.z.object({ entity: ExportEntityRefSchema });
21061
21809
  var deviceExportCapability = {
21062
21810
  name: "device-export",
21063
21811
  scope: "system",
@@ -21087,6 +21835,16 @@ var deviceExportCapability = {
21087
21835
  unexposeDevice: require_sleep.method(UnexposeInputSchema, zod.z.void(), {
21088
21836
  kind: "mutation",
21089
21837
  auth: "admin"
21838
+ }),
21839
+ listSupportedEntityKinds: require_sleep.optionalMethod(zod.z.void(), zod.z.array(ExportEntityKindSchema)),
21840
+ listExposedEntities: require_sleep.optionalMethod(zod.z.void(), zod.z.array(ExposedEntitySchema)),
21841
+ exposeEntity: require_sleep.optionalMethod(ExposeEntityInputSchema, zod.z.void(), {
21842
+ kind: "mutation",
21843
+ auth: "admin"
21844
+ }),
21845
+ unexposeEntity: require_sleep.optionalMethod(UnexposeEntityInputSchema, zod.z.void(), {
21846
+ kind: "mutation",
21847
+ auth: "admin"
21090
21848
  })
21091
21849
  }
21092
21850
  };
@@ -43463,6 +44221,64 @@ var embeddingEncoderCapability = {
43463
44221
  }
43464
44222
  };
43465
44223
  //#endregion
44224
+ //#region src/capabilities/node-removal-hook.cap.ts
44225
+ /**
44226
+ * `node-removal-hook` — the reliable way an addon learns a cluster node was
44227
+ * FORGOTTEN, so it can remove or re-point what it keeps for that node. The
44228
+ * node-scoped twin of `device-removal-hook` (D865); same shape, same rules.
44229
+ *
44230
+ * ## Why not "the node is absent"
44231
+ *
44232
+ * A node missing from topology is offline, restarting or not yet re-registered —
44233
+ * it is not forgotten. Deleting node-scoped data from absence destroys a live
44234
+ * node's configuration on any failed or partial read (D49). So the hook is
44235
+ * driven by a TOMBSTONE the hub records when the operator forgets a node
44236
+ * (`AgentRegistryService.forgetClusterNode`); nothing else produces one.
44237
+ *
44238
+ * ## The pieces
44239
+ *
44240
+ * 1. The hub records a tombstone `{ nodeId, removedAt, pending }` before it calls
44241
+ * anyone. `pending` is the set of implementers that have not yet acknowledged.
44242
+ * 2. The hub calls `onNodeRemoved` on EVERY provider of this collection, one by
44243
+ * one, with bounded retries, logged; an acknowledgement removes the
44244
+ * implementer from `pending`.
44245
+ * 3. The hub re-drives still-pending tombstones at boot, so an implementer that
44246
+ * was down at forget time is told when it is back.
44247
+ *
44248
+ * ## Contract for an implementer
44249
+ *
44250
+ * - **Idempotent.** Called again by a retry and by the boot re-drive. A node with
44251
+ * nothing left answers zero counts.
44252
+ * - **One key per store** in `counts`, rows changed per store, so the log line
44253
+ * can say where the data was.
44254
+ * - **Re-point or remove, never guess.** A row that merely MENTIONS the node as
44255
+ * a preference is re-pointed to "unset"; a row that only means something on
44256
+ * that node (a runtime bound to it) is removed.
44257
+ * - A provider that cannot tell is allowed to throw; the caller retries it.
44258
+ */
44259
+ /** What a hook is told about the removal. */
44260
+ var NodeRemovedInputSchema = zod.z.object({
44261
+ nodeId: zod.z.string().min(1),
44262
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
44263
+ removedAt: zod.z.number()
44264
+ });
44265
+ /** Rows changed (removed or re-pointed), per store this implementer owns. */
44266
+ var NodeRemovedResultSchema = zod.z.object({ counts: zod.z.record(zod.z.string(), zod.z.number().int().nonnegative()) });
44267
+ var nodeRemovalHookCapability = {
44268
+ name: "node-removal-hook",
44269
+ scope: "system",
44270
+ mode: "collection",
44271
+ internal: true,
44272
+ methods: {
44273
+ /** Remove or re-point everything this addon keeps for the forgotten node. Idempotent. */
44274
+ onNodeRemoved: require_sleep.method(NodeRemovedInputSchema, NodeRemovedResultSchema, {
44275
+ kind: "mutation",
44276
+ auth: "admin"
44277
+ }) },
44278
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
44279
+ mount: { kind: "skip" }
44280
+ };
44281
+ //#endregion
43466
44282
  //#region src/capabilities/device-removal-hook.cap.ts
43467
44283
  /**
43468
44284
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
@@ -44737,7 +45553,7 @@ var LlmProviderWidgetSchema = zod.z.object({
44737
45553
  /** ConfigUISchema tree passed through untyped on the wire (the
44738
45554
  * notification-output `ConfigSchemaPassthrough` precedent at
44739
45555
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
44740
- var ConfigSchemaPassthrough$3 = zod.z.unknown();
45556
+ var ConfigSchemaPassthrough$2 = zod.z.unknown();
44741
45557
  var LlmProfileKindDescriptorSchema = zod.z.object({
44742
45558
  kind: LlmProfileKindSchema,
44743
45559
  label: zod.z.string(),
@@ -44746,9 +45562,9 @@ var LlmProfileKindDescriptorSchema = zod.z.object({
44746
45562
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
44747
45563
  addonId: zod.z.string(),
44748
45564
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
44749
- connectionSchema: ConfigSchemaPassthrough$3,
45565
+ connectionSchema: ConfigSchemaPassthrough$2,
44750
45566
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
44751
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
45567
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
44752
45568
  /** The kind's defaults; a model from `listModels` may override any of them. */
44753
45569
  features: LlmModelFeaturesSchema,
44754
45570
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -45505,10 +46321,10 @@ var CpuBreakdownSchema = zod.z.object({
45505
46321
  cores: zod.z.number()
45506
46322
  });
45507
46323
  var MemoryInfoSchema = zod.z.object({
45508
- percent: zod.z.number(),
46324
+ percent: zod.z.number().nullable(),
45509
46325
  totalBytes: zod.z.number(),
45510
- usedBytes: zod.z.number(),
45511
- availableBytes: zod.z.number(),
46326
+ usedBytes: zod.z.number().nullable(),
46327
+ availableBytes: zod.z.number().nullable(),
45512
46328
  swapUsedBytes: zod.z.number(),
45513
46329
  swapTotalBytes: zod.z.number()
45514
46330
  });
@@ -45812,8 +46628,9 @@ var GetLoadSeriesInputSchema = zod.z.object({
45812
46628
  });
45813
46629
  var SystemMetricsSchema = zod.z.object({
45814
46630
  cpuPercent: zod.z.number(),
45815
- memoryPercent: zod.z.number(),
45816
- memoryUsedMB: zod.z.number(),
46631
+ /** `null` when the memory sample failed or was not taken yet (D393). */
46632
+ memoryPercent: zod.z.number().nullable(),
46633
+ memoryUsedMB: zod.z.number().nullable(),
45817
46634
  memoryTotalMB: zod.z.number(),
45818
46635
  diskPercent: zod.z.number().optional(),
45819
46636
  temperature: zod.z.number().optional(),
@@ -46215,453 +47032,6 @@ var networkAccessCapability = {
46215
47032
  }
46216
47033
  };
46217
47034
  //#endregion
46218
- //#region src/capabilities/notification-output.cap.ts
46219
- /**
46220
- * notification-output — canonical, capability-gated notification delivery.
46221
- *
46222
- * Apprise-derived model (see
46223
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
46224
- * callers emit ONE canonical `Notification`; each provider declares a
46225
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
46226
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
46227
- * message to what the kind supports — callers never special-case a service.
46228
- *
46229
- * DESIGN DECISIONS (locked):
46230
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
46231
- * `setTargetEnabled`), each provider persisting via the `settings-store`
46232
- * cap. Rationale: the admin UI needs one uniform surface across the
46233
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
46234
- * alternative would fork the UI per addon and cannot host the
46235
- * discovery→adopt flow.
46236
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
46237
- * the generated cap-mount auto-`concatCollection`-fans them across every
46238
- * registered provider (notifiers addon + HA addon) so one catalog is
46239
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
46240
- * `addonId` the generated collection router extracts from the call input.
46241
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
46242
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
46243
- * `storage` / `storage-provider` / `recording` caps over the same path. No
46244
- * base64 fallback needed.
46245
- *
46246
- * TODO (deferred, closed-set change — separate decision): add
46247
- * `providerKind: 'notify'` so notification providers surface on the unified
46248
- * admin "Integrations" page.
46249
- */
46250
- /**
46251
- * Zentik-derived typed-media enum — the superset across every kind. Each
46252
- * adapter picks what it supports and the degrade engine filters the rest.
46253
- */
46254
- var AttachmentMediaTypeSchema = zod.z.enum([
46255
- "image",
46256
- "video",
46257
- "gif",
46258
- "audio",
46259
- "icon"
46260
- ]);
46261
- /**
46262
- * A single attachment. Exactly one of `url` (remote source, most adapters
46263
- * prefer this) or `bytes` (inline source; required for Pushover-style
46264
- * bytes-only kinds) MUST be present — the degrade engine expresses a
46265
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
46266
- */
46267
- var AttachmentSchema = zod.z.object({
46268
- mediaType: AttachmentMediaTypeSchema,
46269
- url: zod.z.string().optional(),
46270
- bytes: zod.z.instanceof(Uint8Array).optional(),
46271
- mime: zod.z.string().optional(),
46272
- name: zod.z.string().optional()
46273
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
46274
- var NotificationFormatSchema = zod.z.enum([
46275
- "text",
46276
- "markdown",
46277
- "html"
46278
- ]);
46279
- /**
46280
- * The CLOSED icon vocabulary an action button may use.
46281
- *
46282
- * A closed set, not a free string, and that is the whole point: an arbitrary
46283
- * icon name is one that ntfy renders, zentik silently drops, and nobody
46284
- * notices — the same class of gap as a zone vocabulary nothing produced
46285
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
46286
- * declares `actionIcons: false` and the degrade engine strips the field.
46287
- *
46288
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
46289
- * renderer's icon set; "acknowledge" survives an adapter that draws it
46290
- * differently.
46291
- *
46292
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
46293
- *
46294
- * These names are for US. **No adapter may forward one verbatim.** Each maps
46295
- * the whole set onto its own renderer's vocabulary through a
46296
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
46297
- * fallback, so adding a member here fails every adapter's build until someone
46298
- * decides its glyph, which is the only place that decision can be made
46299
- * honestly.
46300
- *
46301
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
46302
- * `disarm` straight through; iOS feeds that string to
46303
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
46304
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
46305
- * mapping, and "the field is documented" is not "the value renders".
46306
- *
46307
- * Adding a member is TRAIN-BOUND. The enum lives in the published
46308
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
46309
- * so an addon that emits a token the running hub does not know does not lose an
46310
- * icon — its whole `send` fails Zod validation and the notification never
46311
- * arrives. Never emit a new token from an addon before the train carrying it.
46312
- */
46313
- var NotificationActionIconSchema = zod.z.enum([
46314
- "acknowledge",
46315
- "dismiss",
46316
- "silence",
46317
- "snooze",
46318
- "view",
46319
- "play",
46320
- "open",
46321
- "close",
46322
- "lock",
46323
- "unlock",
46324
- "arm",
46325
- "arm-home",
46326
- "arm-away",
46327
- "arm-night",
46328
- "disarm",
46329
- "light",
46330
- "alert",
46331
- "camera"
46332
- ]);
46333
- /** A single tap-through action button. */
46334
- var NotificationActionSchema = zod.z.object({
46335
- id: zod.z.string(),
46336
- label: zod.z.string(),
46337
- url: zod.z.string().optional(),
46338
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
46339
- icon: NotificationActionIconSchema.optional(),
46340
- /**
46341
- * Renders in a warning style where the notifier supports it.
46342
- *
46343
- * A HINT, never a gate. The callback's authority is its token and nothing
46344
- * else — see `notification-center/action-token.ts` for what that does and
46345
- * does not buy.
46346
- */
46347
- destructive: zod.z.boolean().optional(),
46348
- /**
46349
- * How the tap should REACH the url.
46350
- *
46351
- * `navigate` (absent, and every button authored before this field) opens it:
46352
- * the phone leaves the notification and shows whatever the callback returns.
46353
- * That is right for a button whose answer the operator wants to read.
46354
- *
46355
- * `background` fires it as a POST and stays put. It exists for the buttons
46356
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
46357
- * an answer to the notification, and being thrown into a browser tab to
46358
- * confirm it costs more attention than the notification did. A backend that
46359
- * cannot do a background call renders it as an ordinary link (the adapters
46360
- * fall back rather than dropping the button), so this is a preference, never
46361
- * a requirement.
46362
- */
46363
- mode: zod.z.enum(["navigate", "background"]).optional()
46364
- });
46365
- /** A delivery parameter value: what a notifier declared it takes. */
46366
- var DeliveryParamValueSchema = zod.z.union([
46367
- zod.z.string(),
46368
- zod.z.number(),
46369
- zod.z.boolean()
46370
- ]);
46371
- /**
46372
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
46373
- * addresses a target of that kind may set it; a kind that declares nothing is
46374
- * never handed one. The admin UI derives its control from this descriptor —
46375
- * no editor knows a service by name.
46376
- *
46377
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
46378
- * run on the addon cap path).
46379
- */
46380
- var DeliveryParamSchema = zod.z.object({
46381
- key: zod.z.string().min(1),
46382
- label: zod.z.string(),
46383
- /** i18n key of the label, resolved by the declaring addon's namespace. */
46384
- labelKey: zod.z.string().optional(),
46385
- type: zod.z.enum([
46386
- "enum",
46387
- "number",
46388
- "boolean",
46389
- "string"
46390
- ]),
46391
- /** `enum` only: the closed set of accepted values. */
46392
- options: zod.z.array(zod.z.object({
46393
- value: zod.z.string(),
46394
- label: zod.z.string()
46395
- })).optional(),
46396
- /** `number` only. */
46397
- min: zod.z.number().optional(),
46398
- max: zod.z.number().optional(),
46399
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
46400
- default: DeliveryParamValueSchema.optional()
46401
- });
46402
- /**
46403
- * The canonical notification. `body` is the only hard field (Apprise model).
46404
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
46405
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
46406
- * the adapter maps this ordinal onto its native level. `level?` is an
46407
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
46408
- * `priority` for that one target.
46409
- */
46410
- var NotificationSchema = zod.z.object({
46411
- body: zod.z.string(),
46412
- title: zod.z.string().optional(),
46413
- format: NotificationFormatSchema.default("text"),
46414
- priority: zod.z.number().int().min(1).max(5).default(3),
46415
- level: zod.z.string().optional(),
46416
- attachments: zod.z.array(AttachmentSchema).optional(),
46417
- clickUrl: zod.z.string().optional(),
46418
- actions: zod.z.array(NotificationActionSchema).optional(),
46419
- sound: zod.z.string().optional(),
46420
- ttl: zod.z.number().optional(),
46421
- tag: zod.z.string().optional(),
46422
- deviceId: zod.z.number().optional(),
46423
- eventId: zod.z.string().optional(),
46424
- metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
46425
- /**
46426
- * Values for the parameters the target's kind DECLARES in
46427
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
46428
- * declaration by the sender; a notifier reads only the keys it declared.
46429
- */
46430
- delivery: zod.z.record(zod.z.string(), DeliveryParamValueSchema).optional()
46431
- });
46432
- /** One declared native severity/priority level for a kind. */
46433
- var TargetKindLevelSchema = zod.z.object({
46434
- id: zod.z.string(),
46435
- label: zod.z.string(),
46436
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
46437
- ordinal: zod.z.number().int().min(1).max(5).nullable(),
46438
- flags: zod.z.object({
46439
- critical: zod.z.boolean().optional(),
46440
- silent: zod.z.boolean().optional(),
46441
- noPush: zod.z.boolean().optional()
46442
- }).optional(),
46443
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
46444
- requires: zod.z.array(zod.z.string()).optional(),
46445
- description: zod.z.string().optional()
46446
- });
46447
- /** Attachment capabilities for a kind (drives the degrade engine + test panel). */
46448
- var TargetKindAttachmentsCapsSchema = zod.z.object({
46449
- mediaTypes: zod.z.array(AttachmentMediaTypeSchema),
46450
- mode: zod.z.enum([
46451
- "url",
46452
- "bytes",
46453
- "both"
46454
- ]),
46455
- max: zod.z.number().int().nonnegative(),
46456
- maxBytes: zod.z.number().int().positive().optional()
46457
- });
46458
- /** The full capability block consulted before dispatch. */
46459
- var TargetKindCapsSchema = zod.z.object({
46460
- attachments: TargetKindAttachmentsCapsSchema,
46461
- /** Max action buttons (0 = none). */
46462
- actions: zod.z.number().int().nonnegative(),
46463
- /**
46464
- * Whether this kind renders a per-action ICON.
46465
- *
46466
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
46467
- * run on the addon cap path — three production failures in one day taught
46468
- * this repo that once. Absent is read as false by the degrade engine, which
46469
- * is the safe direction: an icon that is not rendered costs nothing, an icon
46470
- * assumed and dropped costs the operator's trust in the field.
46471
- */
46472
- actionIcons: zod.z.boolean().optional(),
46473
- levels: zod.z.array(TargetKindLevelSchema),
46474
- format: zod.z.array(NotificationFormatSchema),
46475
- clickUrl: zod.z.boolean(),
46476
- sound: zod.z.boolean(),
46477
- ttl: zod.z.boolean(),
46478
- bodyMaxLen: zod.z.number().int().positive()
46479
- });
46480
- /**
46481
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
46482
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
46483
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
46484
- * the union is large and not meant for runtime validation here; the exported
46485
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
46486
- */
46487
- var ConfigSchemaPassthrough$2 = zod.z.unknown();
46488
- var TargetKindSchema = zod.z.object({
46489
- kind: zod.z.string(),
46490
- label: zod.z.string(),
46491
- icon: zod.z.string(),
46492
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
46493
- addonId: zod.z.string(),
46494
- /**
46495
- * URL of the kind's bundled BRAND icon, served by the providing addon over
46496
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
46497
- * when the addon bundles no icon for that kind — the client then falls back
46498
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
46499
- *
46500
- * Root-relative on purpose: it resolves against whatever origin serves a web
46501
- * client, and a native client joins it onto its own hub base.
46502
- *
46503
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
46504
- * field that survived only because the runtime cap-router forwards provider
46505
- * output verbatim — so every consumer had to re-declare it by hand to stop
46506
- * its own Zod parse from stripping it, and the whole arrangement would have
46507
- * broken silently the moment output validation was tightened anywhere.
46508
- */
46509
- iconUrl: zod.z.string().optional(),
46510
- /**
46511
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
46512
- *
46513
- * The server knows this and therefore says it, because the client cannot
46514
- * safely guess: a React-Native client renders SVG and raster through two
46515
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
46516
- * not decode SVG on iOS/Android), so without this it silently fell back to a
46517
- * placeholder glyph for every vector icon while the web build looked fine.
46518
- *
46519
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
46520
- * not been updated — a client that cannot determine the type should prefer
46521
- * its raster path, which is the safe default for an unknown image.
46522
- */
46523
- iconMediaType: zod.z.string().optional(),
46524
- configSchema: ConfigSchemaPassthrough$2,
46525
- /**
46526
- * The config keys that decide WHERE this kind's secrets are sent (server,
46527
- * base URL, instance, chat, recipient). A save that changes any of them
46528
- * while leaving a secret redacted is refused by the provider (D634), so an
46529
- * editor uses this to ask for the secret on exactly those edits instead of
46530
- * on every edit.
46531
- *
46532
- * Optional: absent from a provider that holds no secret (the Home Assistant
46533
- * kinds) and from a provider older than this field — an editor that finds
46534
- * it absent must assume ANY edit may need the secret.
46535
- */
46536
- destinationKeys: zod.z.array(zod.z.string()).optional(),
46537
- supportsDiscovery: zod.z.boolean(),
46538
- /**
46539
- * Extra delivery parameters a rule may set for a target of this kind (D938).
46540
- * Absent = none; the rule editor then offers nothing.
46541
- */
46542
- deliveryParams: zod.z.array(DeliveryParamSchema).optional(),
46543
- caps: TargetKindCapsSchema
46544
- });
46545
- /**
46546
- * A persisted target. `config` holds secrets; providers REDACT secret fields
46547
- * (return a presence marker only) when serving `listTargets` — never
46548
- * round-trip a stored secret to the UI.
46549
- */
46550
- var TargetSchema = zod.z.object({
46551
- id: zod.z.string(),
46552
- name: zod.z.string(),
46553
- kind: zod.z.string(),
46554
- addonId: zod.z.string(),
46555
- enabled: zod.z.boolean(),
46556
- config: zod.z.record(zod.z.string(), zod.z.unknown())
46557
- });
46558
- /** A discovery-surfaced candidate (config is partial + non-secret). */
46559
- var DiscoveredTargetSchema = zod.z.object({
46560
- kind: zod.z.string(),
46561
- suggestedName: zod.z.string(),
46562
- config: zod.z.record(zod.z.string(), zod.z.unknown())
46563
- });
46564
- /** The degrade engine's report — what was resolved / dropped / degraded. */
46565
- var RenderedAsSchema = zod.z.object({
46566
- level: zod.z.string(),
46567
- format: NotificationFormatSchema,
46568
- attachmentsSent: zod.z.number().int().nonnegative(),
46569
- actionsSent: zod.z.number().int().nonnegative(),
46570
- truncated: zod.z.boolean(),
46571
- dropped: zod.z.array(zod.z.string())
46572
- });
46573
- var SendResultSchema = zod.z.object({
46574
- success: zod.z.boolean(),
46575
- error: zod.z.string().optional(),
46576
- renderedAs: RenderedAsSchema.optional()
46577
- });
46578
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
46579
- var TestResultSchema = SendResultSchema;
46580
- /**
46581
- * Pre-save credential probe against a partial (unsaved) target config.
46582
- * `resolvedUrl` never includes secrets (token stripped from path).
46583
- */
46584
- var ProbeTargetConfigResultSchema = zod.z.object({
46585
- ok: zod.z.boolean(),
46586
- resolvedUrl: zod.z.string(),
46587
- httpStatus: zod.z.number().int().optional(),
46588
- stateInstance: zod.z.string().optional(),
46589
- error: zod.z.string().optional()
46590
- });
46591
- var notificationOutputCapability = {
46592
- name: "notification-output",
46593
- scope: "system",
46594
- mode: "collection",
46595
- methods: {
46596
- listTargetKinds: require_sleep.method(zod.z.object({}), zod.z.array(TargetKindSchema)),
46597
- listTargets: require_sleep.method(zod.z.object({}), zod.z.array(TargetSchema), { auth: "admin" }),
46598
- /**
46599
- * One target WITH secrets — admin edit / duplicate hydration only.
46600
- * `listTargets` stays redacted; the editor loads this so password fields
46601
- * are filled (masked in the input) and a save can round-trip the token
46602
- * when a destination changes (Duplicate with a new chatId, etc.).
46603
- */
46604
- getTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), TargetSchema, { auth: "admin" }),
46605
- discoverTargets: require_sleep.method(zod.z.object({
46606
- kind: zod.z.string(),
46607
- config: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
46608
- /**
46609
- * When editing / duplicating, the blank password field never round-trips
46610
- * the stored token. Pass the source target id so the provider can restore
46611
- * secrets server-side before dialling (same merge as upsert).
46612
- */
46613
- targetId: zod.z.string().optional()
46614
- }), zod.z.array(DiscoveredTargetSchema), { auth: "admin" }),
46615
- probeTargetConfig: require_sleep.method(zod.z.object({
46616
- kind: zod.z.string(),
46617
- config: zod.z.record(zod.z.string(), zod.z.unknown()),
46618
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
46619
- targetId: zod.z.string().optional()
46620
- }), ProbeTargetConfigResultSchema, {
46621
- kind: "mutation",
46622
- auth: "admin"
46623
- }),
46624
- send: require_sleep.method(zod.z.object({
46625
- targetId: zod.z.string(),
46626
- notification: NotificationSchema
46627
- }), SendResultSchema, {
46628
- kind: "mutation",
46629
- auth: "admin"
46630
- }),
46631
- testTarget: require_sleep.method(zod.z.object({
46632
- targetId: zod.z.string(),
46633
- sample: NotificationSchema.optional()
46634
- }), TestResultSchema, {
46635
- kind: "mutation",
46636
- auth: "admin"
46637
- }),
46638
- upsertTarget: require_sleep.method(zod.z.object({
46639
- target: TargetSchema,
46640
- /**
46641
- * Duplicate flow: create a NEW target id while copying secrets from an
46642
- * existing row. The UI never holds the stored token; blank / redacted
46643
- * password fields are restored from this source (D634 still applies if
46644
- * destinations diverge without a re-typed secret).
46645
- */
46646
- cloneSecretsFrom: zod.z.string().optional()
46647
- }), TargetSchema, {
46648
- kind: "mutation",
46649
- auth: "admin"
46650
- }),
46651
- deleteTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), zod.z.void(), {
46652
- kind: "mutation",
46653
- auth: "admin"
46654
- }),
46655
- setTargetEnabled: require_sleep.method(zod.z.object({
46656
- targetId: zod.z.string(),
46657
- enabled: zod.z.boolean()
46658
- }), zod.z.void(), {
46659
- kind: "mutation",
46660
- auth: "admin"
46661
- })
46662
- }
46663
- };
46664
- //#endregion
46665
47035
  //#region src/catalogs/audio-classmap.ts
46666
47036
  var AUDIO_MACRO_LABELS = [
46667
47037
  {
@@ -55536,14 +55906,6 @@ var EngineProvisioningSchema = zod.z.object({
55536
55906
  */
55537
55907
  configIssues: zod.z.array(zod.z.string()).optional()
55538
55908
  });
55539
- var PipelineStepInputSchema = zod.z.lazy(() => zod.z.object({
55540
- addonId: zod.z.string(),
55541
- modelId: zod.z.string().optional(),
55542
- enabled: zod.z.boolean().default(true),
55543
- children: zod.z.array(PipelineStepInputSchema).optional(),
55544
- settings: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
55545
- jumpDeviceKey: zod.z.string().optional()
55546
- }));
55547
55909
  var ModelSubstitutionSchema = zod.z.object({
55548
55910
  addonId: zod.z.string(),
55549
55911
  chosen: zod.z.string(),
@@ -55622,7 +55984,6 @@ var ConfigUISchemaBridge = zod.z.custom();
55622
55984
  var ConfigUISchemaNullableBridge = zod.z.custom();
55623
55985
  var InferenceCapabilitiesBridge = zod.z.custom();
55624
55986
  var ModelAvailabilityListBridge = zod.z.custom();
55625
- var PipelineRunResultBridge = zod.z.custom();
55626
55987
  /**
55627
55988
  * Pipeline executor — detection engine + configuration + inference API.
55628
55989
  *
@@ -55735,7 +56096,7 @@ var pipelineExecutorCapability = {
55735
56096
  * reads any media: can this (node, deviceKey) run exactly this model?
55736
56097
  */
55737
56098
  validatePipeline: require_sleep.method(zod.z.object({
55738
- steps: zod.z.array(PipelineStepInputSchema),
56099
+ steps: zod.z.array(require_sleep.PipelineStepInputSchema),
55739
56100
  deviceKey: zod.z.string().optional()
55740
56101
  }), PipelineValidationResultSchema),
55741
56102
  getCapabilities: require_sleep.method(zod.z.void(), InferenceCapabilitiesBridge),
@@ -55779,7 +56140,7 @@ var pipelineExecutorCapability = {
55779
56140
  * - `referenceImage`: named file from the reference-image store.
55780
56141
  */
55781
56142
  runPipeline: require_sleep.method(zod.z.object({
55782
- steps: zod.z.array(PipelineStepInputSchema).min(1),
56143
+ steps: zod.z.array(require_sleep.PipelineStepInputSchema).min(1),
55783
56144
  frame: FrameInputSchema.optional(),
55784
56145
  /**
55785
56146
  * Process-local lazy frame. Valid only when caller and provider resolve
@@ -55853,7 +56214,7 @@ var pipelineExecutorCapability = {
55853
56214
  * PIXELS of the image this call carries. Absent ⇒ the root step runs.
55854
56215
  */
55855
56216
  suppliedRoot: SuppliedRootOutputSchema.optional()
55856
- }), PipelineRunResultBridge, { kind: "mutation" }),
56217
+ }), require_sleep.PipelineRunResultBridge, { kind: "mutation" }),
55857
56218
  /**
55858
56219
  * Batched run — N raw frames packed into one cap call. The provider
55859
56220
  * routes the batch through `SharedInferencePool.inferBatch`
@@ -55866,7 +56227,7 @@ var pipelineExecutorCapability = {
55866
56227
  * N frames in one call to amortise per-call IPC overhead.
55867
56228
  */
55868
56229
  runPipelineBatch: require_sleep.method(zod.z.object({
55869
- steps: zod.z.array(PipelineStepInputSchema).min(1),
56230
+ steps: zod.z.array(require_sleep.PipelineStepInputSchema).min(1),
55870
56231
  frames: zod.z.array(FrameInputSchema).min(1).max(255),
55871
56232
  deviceId: zod.z.number().optional(),
55872
56233
  sessionId: zod.z.string().optional(),
@@ -55883,7 +56244,7 @@ var pipelineExecutorCapability = {
55883
56244
  frameId: zod.z.number().int().nonnegative().optional(),
55884
56245
  /** Inference-device selector (Phase 2 multi-device); see runPipeline. */
55885
56246
  deviceKey: zod.z.string().optional()
55886
- }), zod.z.object({ results: zod.z.array(PipelineRunResultBridge).readonly() }), { kind: "mutation" }),
56247
+ }), zod.z.object({ results: zod.z.array(require_sleep.PipelineRunResultBridge).readonly() }), { kind: "mutation" }),
55887
56248
  /**
55888
56249
  * Cache a raw frame inside the Python inference pool's memory.
55889
56250
  * Returns a numeric `frameId` that `inferCached` references —
@@ -57586,7 +57947,7 @@ var RunnerCameraConfigSchema = zod.z.object({
57586
57947
  detectionSources: DetectionSourcesSchema.default(["pipeline"]),
57587
57948
  pipelineEnabled: zod.z.boolean().default(true),
57588
57949
  /** Ordered tree of video steps. Absent → runner skips video detection. */
57589
- steps: zod.z.array(PipelineStepInputSchema).readonly().optional(),
57950
+ steps: zod.z.array(require_sleep.PipelineStepInputSchema).readonly().optional(),
57590
57951
  /** Audio classification branch. `enabled:false` disables, null skips. */
57591
57952
  audio: zod.z.object({
57592
57953
  modelId: zod.z.string().optional(),
@@ -58321,7 +58682,7 @@ var pipelineRunnerCapability = {
58321
58682
  //#region src/capabilities/pipeline-orchestrator.cap.ts
58322
58683
  var CameraPipelineConfigSchema = zod.z.object({
58323
58684
  engine: PipelineEngineChoiceSchema.optional(),
58324
- steps: zod.z.array(PipelineStepInputSchema).readonly(),
58685
+ steps: zod.z.array(require_sleep.PipelineStepInputSchema).readonly(),
58325
58686
  audio: zod.z.object({
58326
58687
  engine: PipelineEngineChoiceSchema.optional(),
58327
58688
  modelId: zod.z.string().optional(),
@@ -59120,8 +59481,7 @@ var pipelineOrchestratorCapability = {
59120
59481
  * replaced the four flat orchestrator lists (`enabledNodes`,
59121
59482
  * `enabledDecoderNodes`, `enabledAudioNodes`, `remoteSourcingNodes`).
59122
59483
  * Each flag is optional in the patch: omit a flag to leave it unchanged,
59123
- * pass `null` to reset it to the node default (`hub` → capable, every
59124
- * other node → not). Detection/audio eligibility is derived from these
59484
+ * pass `null` to reset it to the default (ON for every node). Detection/audio eligibility is derived from these
59125
59485
  * flags across the cluster; changing them re-derives the enabled sets
59126
59486
  * and reconciles dispatch immediately. There is NO separate decode flag:
59127
59487
  * decode is always co-located with its frame consumer, so decode
@@ -66927,14 +67287,13 @@ reset: require_sleep.method(zod.z.object({
66927
67287
  auth: "admin"
66928
67288
  }) },
66929
67289
  /**
66930
- * A composed consumable (item added to a device with no native consumables
66931
- * provider) refuses `reset` by name: "Only meaningful when the item's
66932
- * `resettable` is true" — a composed item is never resettable until
66933
- * commands (`forward`) arrive in slice 3 (D663).
67290
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
67291
+ * name: only a composed device of a NEW type forwards an item's reset to a
67292
+ * source button, and marks that item `resettable` (D967).
66934
67293
  */
66935
67294
  composedMethods: { reset: {
66936
67295
  kind: "refuse",
66937
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
67296
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
66938
67297
  } },
66939
67298
  status: {
66940
67299
  schema: ConsumablesStatusSchema,
@@ -67597,7 +67956,7 @@ var DishwasherStatusSchema = zod.z.object({
67597
67956
  /** Ms epoch of the last value received from the appliance; 0 = never. */
67598
67957
  lastFetchedAt: zod.z.number()
67599
67958
  });
67600
- var DeviceIdSchema = zod.z.number().int().nonnegative();
67959
+ var DeviceIdSchema$1 = zod.z.number().int().nonnegative();
67601
67960
  /** A value a program option or setting can take. */
67602
67961
  var DishwasherValueSchema = zod.z.union([
67603
67962
  zod.z.boolean(),
@@ -67672,77 +68031,77 @@ var dishwasherCapability = {
67672
68031
  * option feature keys; each is checked against the program's constraints.
67673
68032
  */
67674
68033
  start: require_sleep.method(zod.z.object({
67675
- deviceId: DeviceIdSchema,
68034
+ deviceId: DeviceIdSchema$1,
67676
68035
  program: zod.z.string().min(1).optional(),
67677
68036
  options: zod.z.record(zod.z.string(), DishwasherValueSchema).optional()
67678
68037
  }), zod.z.void(), {
67679
68038
  kind: "mutation",
67680
68039
  auth: "protected"
67681
68040
  }),
67682
- pause: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
68041
+ pause: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema$1 }), zod.z.void(), {
67683
68042
  kind: "mutation",
67684
68043
  auth: "protected"
67685
68044
  }),
67686
- resume: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
68045
+ resume: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema$1 }), zod.z.void(), {
67687
68046
  kind: "mutation",
67688
68047
  auth: "protected"
67689
68048
  }),
67690
- stop: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
68049
+ stop: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema$1 }), zod.z.void(), {
67691
68050
  kind: "mutation",
67692
68051
  auth: "protected"
67693
68052
  }),
67694
68053
  /** Select (not start) a program; refused when the appliance does not offer it. */
67695
68054
  setProgram: require_sleep.method(zod.z.object({
67696
- deviceId: DeviceIdSchema,
68055
+ deviceId: DeviceIdSchema$1,
67697
68056
  program: zod.z.string().min(1)
67698
68057
  }), zod.z.void(), {
67699
68058
  kind: "mutation",
67700
68059
  auth: "protected"
67701
68060
  }),
67702
68061
  setPower: require_sleep.method(zod.z.object({
67703
- deviceId: DeviceIdSchema,
68062
+ deviceId: DeviceIdSchema$1,
67704
68063
  on: zod.z.boolean()
67705
68064
  }), zod.z.void(), {
67706
68065
  kind: "mutation",
67707
68066
  auth: "protected"
67708
68067
  }),
67709
68068
  setHalfLoad: require_sleep.method(zod.z.object({
67710
- deviceId: DeviceIdSchema,
68069
+ deviceId: DeviceIdSchema$1,
67711
68070
  on: zod.z.boolean()
67712
68071
  }), zod.z.void(), {
67713
68072
  kind: "mutation",
67714
68073
  auth: "protected"
67715
68074
  }),
67716
68075
  setHygienePlus: require_sleep.method(zod.z.object({
67717
- deviceId: DeviceIdSchema,
68076
+ deviceId: DeviceIdSchema$1,
67718
68077
  on: zod.z.boolean()
67719
68078
  }), zod.z.void(), {
67720
68079
  kind: "mutation",
67721
68080
  auth: "protected"
67722
68081
  }),
67723
68082
  setExtraDry: require_sleep.method(zod.z.object({
67724
- deviceId: DeviceIdSchema,
68083
+ deviceId: DeviceIdSchema$1,
67725
68084
  on: zod.z.boolean()
67726
68085
  }), zod.z.void(), {
67727
68086
  kind: "mutation",
67728
68087
  auth: "protected"
67729
68088
  }),
67730
68089
  setIntensiveZone: require_sleep.method(zod.z.object({
67731
- deviceId: DeviceIdSchema,
68090
+ deviceId: DeviceIdSchema$1,
67732
68091
  on: zod.z.boolean()
67733
68092
  }), zod.z.void(), {
67734
68093
  kind: "mutation",
67735
68094
  auth: "protected"
67736
68095
  }),
67737
68096
  setVarioSpeedPlus: require_sleep.method(zod.z.object({
67738
- deviceId: DeviceIdSchema,
68097
+ deviceId: DeviceIdSchema$1,
67739
68098
  on: zod.z.boolean()
67740
68099
  }), zod.z.void(), {
67741
68100
  kind: "mutation",
67742
68101
  auth: "protected"
67743
68102
  }),
67744
68103
  setChildLock: require_sleep.method(zod.z.object({
67745
- deviceId: DeviceIdSchema,
68104
+ deviceId: DeviceIdSchema$1,
67746
68105
  on: zod.z.boolean()
67747
68106
  }), zod.z.void(), {
67748
68107
  kind: "mutation",
@@ -67754,7 +68113,7 @@ var dishwasherCapability = {
67754
68113
  * its declared range / members.
67755
68114
  */
67756
68115
  setOption: require_sleep.method(zod.z.object({
67757
- deviceId: DeviceIdSchema,
68116
+ deviceId: DeviceIdSchema$1,
67758
68117
  key: zod.z.string().min(1),
67759
68118
  value: DishwasherValueSchema
67760
68119
  }), zod.z.void(), {
@@ -67762,7 +68121,7 @@ var dishwasherCapability = {
67762
68121
  auth: "protected"
67763
68122
  }),
67764
68123
  /** What this appliance can do, from its own description — the UI is derived from it. */
67765
- getOptions: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
68124
+ getOptions: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
67766
68125
  },
67767
68126
  status: {
67768
68127
  schema: DishwasherStatusSchema,
@@ -72065,7 +72424,8 @@ var TopologyNodeSchema = zod.z.object({
72065
72424
  isHub: zod.z.boolean(),
72066
72425
  isOnline: zod.z.boolean(),
72067
72426
  cpuPercent: zod.z.number(),
72068
- memoryPercent: zod.z.number(),
72427
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
72428
+ memoryPercent: zod.z.number().nullable(),
72069
72429
  uptime: zod.z.number(),
72070
72430
  lastSeen: zod.z.string(),
72071
72431
  localIps: zod.z.array(zod.z.string()).readonly(),
@@ -72906,9 +73266,10 @@ var PetFeederStatusSchema = zod.z.object({
72906
73266
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
72907
73267
  * the device has not reported yet. */
72908
73268
  foodLevel: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
72909
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
72910
- * feeder's own units. Null when the model reports none. */
72911
- bowlFood: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
73269
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
73270
+ * feeder with a bowl scale; null when the model has none or its reading is
73271
+ * of an unproven unit. */
73272
+ bowlFood: zod.z.number().nullable().meta(require_sleep.readingMeta("mass")),
72912
73273
  /** True while the pet is eating (`state.eating`). */
72913
73274
  eating: zod.z.boolean(),
72914
73275
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -73093,6 +73454,139 @@ var petFeederCapability = {
73093
73454
  } }
73094
73455
  };
73095
73456
  //#endregion
73457
+ //#region src/capabilities/pet-fountain.cap.ts
73458
+ /**
73459
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
73460
+ * circulating drinking fountain: how much water is in the tank, how much the
73461
+ * pet drank, whether the pump runs and on what duty cycle.
73462
+ *
73463
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
73464
+ * a container of loose entities (a pump switch, two number boxes, a "filter
73465
+ * changed" button), and an operator BUILDS the device from them — a composed
73466
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
73467
+ * method of ONE source entity, and every reading is a field derived from a
73468
+ * source entity; the provider-shaped interface below exists so the composed
73469
+ * device and any future native provider answer the same contract.
73470
+ *
73471
+ * Quantities are stored in the base unit of their kind (D944): volumes in
73472
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
73473
+ * reports millilitres or minutes is converted in the composition's expression,
73474
+ * never here. Every reading may be `null`: a source that is offline or does
73475
+ * not report is unknown, never zero.
73476
+ *
73477
+ * The filter is not a field of this cap: it is an item of the `consumables`
73478
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
73479
+ */
73480
+ var PetFountainStatusSchema = zod.z.object({
73481
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
73482
+ waterLevel: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
73483
+ /** Water in the tank, in litres. Null while the source has not reported. */
73484
+ waterVolume: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("volume")),
73485
+ /** True when the tank is empty. Null while unknown. */
73486
+ empty: zod.z.boolean().nullable(),
73487
+ /** True while the pump is running. Null while unknown. */
73488
+ pumpRunning: zod.z.boolean().nullable(),
73489
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
73490
+ pumpIntervalEnabled: zod.z.boolean().nullable(),
73491
+ /** How long the pump runs per cycle, in seconds. */
73492
+ pumpOnTime: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("duration")),
73493
+ /** How long the pump rests per cycle, in seconds. */
73494
+ pumpOffTime: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("duration")),
73495
+ /** Water drunk in the last drink, in litres. */
73496
+ lastDrink: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("volume")),
73497
+ /** Water drunk in the current hour, in litres. */
73498
+ currentHourDrink: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("volume")),
73499
+ /** Water drunk over the last 24 hours (rolling), in litres. */
73500
+ last24hDrink: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("volume")),
73501
+ /** Water drunk since local midnight, in litres. */
73502
+ todayDrink: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("volume")),
73503
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
73504
+ error: zod.z.string().nullable(),
73505
+ /** Ms epoch when the slice was last refreshed. */
73506
+ lastFetchedAt: zod.z.number()
73507
+ });
73508
+ var DeviceIdSchema = zod.z.number().int().nonnegative();
73509
+ var MinutesSchema = zod.z.number().int().min(0);
73510
+ var petFountainCapability = {
73511
+ name: "pet-fountain",
73512
+ scope: "device",
73513
+ deviceNative: true,
73514
+ mode: "singleton",
73515
+ deviceTypes: [require_err_msg.DeviceType.PetFountain],
73516
+ methods: {
73517
+ /** Start or stop the pump. */
73518
+ setPump: require_sleep.method(zod.z.object({
73519
+ deviceId: DeviceIdSchema,
73520
+ on: zod.z.boolean()
73521
+ }), zod.z.void(), {
73522
+ kind: "mutation",
73523
+ auth: "protected"
73524
+ }),
73525
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
73526
+ setPumpInterval: require_sleep.method(zod.z.object({
73527
+ deviceId: DeviceIdSchema,
73528
+ on: zod.z.boolean()
73529
+ }), zod.z.void(), {
73530
+ kind: "mutation",
73531
+ auth: "protected"
73532
+ }),
73533
+ /** Set how many minutes the pump runs per cycle. */
73534
+ setPumpOnMinutes: require_sleep.method(zod.z.object({
73535
+ deviceId: DeviceIdSchema,
73536
+ minutes: MinutesSchema
73537
+ }), zod.z.void(), {
73538
+ kind: "mutation",
73539
+ auth: "protected"
73540
+ }),
73541
+ /** Set how many minutes the pump rests per cycle. */
73542
+ setPumpOffMinutes: require_sleep.method(zod.z.object({
73543
+ deviceId: DeviceIdSchema,
73544
+ minutes: MinutesSchema
73545
+ }), zod.z.void(), {
73546
+ kind: "mutation",
73547
+ auth: "protected"
73548
+ }),
73549
+ /** Clear the rolling daily consumption counters on the device. */
73550
+ resetDailyConsumption: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
73551
+ kind: "mutation",
73552
+ auth: "protected"
73553
+ })
73554
+ },
73555
+ status: {
73556
+ schema: PetFountainStatusSchema,
73557
+ kind: "push"
73558
+ },
73559
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
73560
+ runtimeState: PetFountainStatusSchema,
73561
+ /**
73562
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
73563
+ *
73564
+ * See `RuntimeStateDurability`. Enforced by
73565
+ * `scripts/check-runtime-state-durability.ts`.
73566
+ */
73567
+ durability: "session",
73568
+ /** History (D850): recorded series. */
73569
+ history: { fields: {
73570
+ waterLevel: {
73571
+ kind: "gauge",
73572
+ unit: "%",
73573
+ deadband: 1
73574
+ },
73575
+ waterVolume: {
73576
+ kind: "gauge",
73577
+ unit: "L"
73578
+ },
73579
+ todayDrink: {
73580
+ kind: "gauge",
73581
+ unit: "L"
73582
+ },
73583
+ last24hDrink: {
73584
+ kind: "gauge",
73585
+ unit: "L"
73586
+ }
73587
+ } }
73588
+ };
73589
+ //#endregion
73096
73590
  //#region src/capabilities/plate-gallery.cap.ts
73097
73591
  var VehicleSchema = zod.z.object({
73098
73592
  id: zod.z.string(),
@@ -82985,7 +83479,7 @@ var ROOM_EXAMPLE_MOTION_SOURCES = [{
82985
83479
  addonId: "provider-homeassistant",
82986
83480
  stableId: "binary_sensor.living_room_presence"
82987
83481
  }];
82988
- function from(source, cap, fieldPath) {
83482
+ function from$1(source, cap, fieldPath) {
82989
83483
  return {
82990
83484
  kind: "from",
82991
83485
  source,
@@ -82994,7 +83488,7 @@ function from(source, cap, fieldPath) {
82994
83488
  };
82995
83489
  }
82996
83490
  function named(prefix, sources, cap, fieldPath) {
82997
- return Object.fromEntries(sources.map((s, i) => [`${prefix}${i + 1}`, from(s, cap, fieldPath)]));
83491
+ return Object.fromEntries(sources.map((s, i) => [`${prefix}${i + 1}`, from$1(s, cap, fieldPath)]));
82998
83492
  }
82999
83493
  function roomCompositionExample(input) {
83000
83494
  const t = named("t", input.temperatureSources, "temperature-sensor", "temperature");
@@ -83082,7 +83576,7 @@ function feederExampleSources(brokerId) {
83082
83576
  };
83083
83577
  }
83084
83578
  function feederCustomizationExample(sources) {
83085
- const days = { d: from(sources.desiccantDays, "enum-sensor", "value") };
83579
+ const days = { d: from$1(sources.desiccantDays, "enum-sensor", "value") };
83086
83580
  return require_composition.CompositionSchema.parse({
83087
83581
  target: {
83088
83582
  kind: "existing",
@@ -83130,7 +83624,7 @@ function feederCustomizationExample(sources) {
83130
83624
  level: {
83131
83625
  kind: "expression",
83132
83626
  expr: "s == \"low\" ? 15 : (s == \"normal\" ? 100 : null)",
83133
- bindings: { s: from(sources.batteryStatus, "enum-sensor", "value") }
83627
+ bindings: { s: from$1(sources.batteryStatus, "enum-sensor", "value") }
83134
83628
  },
83135
83629
  binary: {
83136
83630
  kind: "expression",
@@ -83140,7 +83634,7 @@ function feederCustomizationExample(sources) {
83140
83634
  charging: {
83141
83635
  kind: "expression",
83142
83636
  expr: "c ? \"dc\" : \"unknown\"",
83143
- bindings: { c: from(sources.batteryCharging, "binary", "on") }
83637
+ bindings: { c: from$1(sources.batteryCharging, "binary", "on") }
83144
83638
  },
83145
83639
  sleeping: {
83146
83640
  kind: "expression",
@@ -83226,6 +83720,158 @@ function convertPresetFeature(input) {
83226
83720
  };
83227
83721
  }
83228
83722
  //#endregion
83723
+ //#region src/composition/presets/pet-fountain.ts
83724
+ /**
83725
+ * The preset for a pet water fountain built from loose entities (D659, D967).
83726
+ *
83727
+ * A fountain reaches CamStack as a container of single-purpose entities: a
83728
+ * pump switch, a cycle switch, two number boxes, a handful of sensors that
83729
+ * report millilitres as text, and a few buttons. This preset names the
83730
+ * `pet-fountain` cap's fields over those entities, forwards each of its five
83731
+ * methods to ONE entity method, and adds the filter as a resettable
83732
+ * `consumables` item.
83733
+ *
83734
+ * Stored quantities are base units (D944): litres for volume, seconds for
83735
+ * duration. The cycle times cannot be read back (see `pumpOnTime` below). The sensors are `enum-sensor`s, so their value is a STRING:
83736
+ * `number(x)` reads it and anything that is not a number stays `null`, never
83737
+ * 0 (D393). The caller supplies the sources — it knows which entity is which;
83738
+ * the preset guesses no entity id.
83739
+ */
83740
+ var UNKNOWN = {
83741
+ kind: "expression",
83742
+ expr: "null",
83743
+ bindings: {}
83744
+ };
83745
+ var NOW = {
83746
+ kind: "expression",
83747
+ expr: "now",
83748
+ bindings: {}
83749
+ };
83750
+ function from(source, cap, fieldPath) {
83751
+ return {
83752
+ kind: "from",
83753
+ source,
83754
+ cap,
83755
+ fieldPath
83756
+ };
83757
+ }
83758
+ function over(name, binding, expr) {
83759
+ return {
83760
+ kind: "expression",
83761
+ expr,
83762
+ bindings: { [name]: binding }
83763
+ };
83764
+ }
83765
+ /** A text reading in millilitres, as litres; unreadable stays unknown. */
83766
+ function millilitresAsLitres(source) {
83767
+ return over("v", from(source, "enum-sensor", "value"), "number(v) == null ? null : number(v) / 1000");
83768
+ }
83769
+ function percentOf(source) {
83770
+ return over("l", from(source, "enum-sensor", "value"), "number(l)");
83771
+ }
83772
+ function filterFeature(filter) {
83773
+ const days = { d: from(filter.daysRemaining, "enum-sensor", "value") };
83774
+ return {
83775
+ kind: "fields",
83776
+ cap: "consumables",
83777
+ fields: {
83778
+ lastChangedAt: NOW,
83779
+ lastFetchedAt: NOW
83780
+ },
83781
+ items: { filter: {
83782
+ label: "Filter",
83783
+ fields: {
83784
+ "remaining.value": {
83785
+ kind: "expression",
83786
+ expr: "number(d)",
83787
+ bindings: days
83788
+ },
83789
+ "remaining.kind": {
83790
+ kind: "expression",
83791
+ expr: "\"duration\"",
83792
+ bindings: {}
83793
+ },
83794
+ status: {
83795
+ kind: "expression",
83796
+ expr: "number(d) == null ? null : (number(d) <= 0 ? \"replace\" : \"ok\")",
83797
+ bindings: days
83798
+ }
83799
+ },
83800
+ reset: {
83801
+ kind: "forward",
83802
+ source: filter.changed,
83803
+ cap: "button",
83804
+ method: "press"
83805
+ }
83806
+ } }
83807
+ };
83808
+ }
83809
+ function petFountainFromEntitiesComposition(sources) {
83810
+ const minutes = {
83811
+ kind: "numeric",
83812
+ value: { $input: "minutes" }
83813
+ };
83814
+ return require_composition.CompositionSchema.parse({
83815
+ target: {
83816
+ kind: "new",
83817
+ type: require_err_msg.DeviceType.PetFountain
83818
+ },
83819
+ features: [{
83820
+ kind: "fields",
83821
+ cap: "pet-fountain",
83822
+ fields: {
83823
+ waterLevel: percentOf(sources.waterLevel),
83824
+ waterVolume: millilitresAsLitres(sources.waterVolume),
83825
+ empty: over("l", from(sources.waterLevel, "enum-sensor", "value"), "number(l) == null ? null : number(l) <= 0"),
83826
+ pumpRunning: from(sources.pump, "switch", "on"),
83827
+ pumpIntervalEnabled: from(sources.pumpInterval, "switch", "on"),
83828
+ pumpOnTime: UNKNOWN,
83829
+ pumpOffTime: UNKNOWN,
83830
+ lastDrink: millilitresAsLitres(sources.lastDrink),
83831
+ currentHourDrink: millilitresAsLitres(sources.currentHourDrink),
83832
+ last24hDrink: millilitresAsLitres(sources.last24hDrink),
83833
+ todayDrink: millilitresAsLitres(sources.todayDrink),
83834
+ error: UNKNOWN,
83835
+ lastFetchedAt: NOW
83836
+ },
83837
+ commands: {
83838
+ setPump: {
83839
+ kind: "forward",
83840
+ source: sources.pump,
83841
+ cap: "switch",
83842
+ method: "setState"
83843
+ },
83844
+ setPumpInterval: {
83845
+ kind: "forward",
83846
+ source: sources.pumpInterval,
83847
+ cap: "switch",
83848
+ method: "setState"
83849
+ },
83850
+ setPumpOnMinutes: {
83851
+ kind: "forward",
83852
+ source: sources.pumpOnMinutes,
83853
+ cap: "control",
83854
+ method: "setValue",
83855
+ args: { control: minutes }
83856
+ },
83857
+ setPumpOffMinutes: {
83858
+ kind: "forward",
83859
+ source: sources.pumpOffMinutes,
83860
+ cap: "control",
83861
+ method: "setValue",
83862
+ args: { control: minutes }
83863
+ },
83864
+ resetDailyConsumption: {
83865
+ kind: "forward",
83866
+ source: sources.clearDaily,
83867
+ cap: "button",
83868
+ method: "press"
83869
+ }
83870
+ }
83871
+ }, ...sources.filter === void 0 ? [] : [filterFeature(sources.filter)]]
83872
+ });
83873
+ }
83874
+ //#endregion
83229
83875
  //#region src/composition/cards/classify-field.ts
83230
83876
  function fromSources(binding) {
83231
83877
  return {
@@ -83475,6 +84121,7 @@ var CAP_CUSTOMIZE_CATALOG = {
83475
84121
  require_err_msg.DeviceType.Container,
83476
84122
  require_err_msg.DeviceType.Image,
83477
84123
  require_err_msg.DeviceType.PetFeeder,
84124
+ require_err_msg.DeviceType.PetFountain,
83478
84125
  require_err_msg.DeviceType.Dishwasher,
83479
84126
  require_err_msg.DeviceType.Washer,
83480
84127
  require_err_msg.DeviceType.Dryer
@@ -85987,6 +86634,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
85987
86634
  notifier: notifierCapability,
85988
86635
  numericSensor: numericSensorCapability,
85989
86636
  petFeeder: petFeederCapability,
86637
+ petFountain: petFountainCapability,
85990
86638
  powerMeter: powerMeterCapability,
85991
86639
  presence: presenceCapability,
85992
86640
  pressureSensor: pressureSensorCapability,
@@ -87975,6 +88623,7 @@ var DEVICE_TYPE_CONTROL_KIND = {
87975
88623
  [require_err_msg.DeviceType.Container]: "dummy",
87976
88624
  [require_err_msg.DeviceType.Image]: "image",
87977
88625
  [require_err_msg.DeviceType.PetFeeder]: "pet-feeder",
88626
+ [require_err_msg.DeviceType.PetFountain]: "pet-fountain",
87978
88627
  [require_err_msg.DeviceType.Dishwasher]: "dishwasher",
87979
88628
  [require_err_msg.DeviceType.Washer]: "dummy",
87980
88629
  [require_err_msg.DeviceType.Dryer]: "dummy"
@@ -90017,6 +90666,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
90017
90666
  access: "create",
90018
90667
  role: "admin"
90019
90668
  },
90669
+ "deviceExport.exposeEntity": {
90670
+ capName: "device-export",
90671
+ capScope: "system",
90672
+ addonId: null,
90673
+ access: "create",
90674
+ role: "admin"
90675
+ },
90020
90676
  "deviceExport.getDeviceLiveContribution": {
90021
90677
  capName: "device-export",
90022
90678
  capScope: "system",
@@ -90045,6 +90701,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
90045
90701
  access: "view",
90046
90702
  role: "admin"
90047
90703
  },
90704
+ "deviceExport.listExposedEntities": {
90705
+ capName: "device-export",
90706
+ capScope: "system",
90707
+ addonId: null,
90708
+ access: "view",
90709
+ role: "admin"
90710
+ },
90048
90711
  "deviceExport.listSupportedDeviceKinds": {
90049
90712
  capName: "device-export",
90050
90713
  capScope: "system",
@@ -90052,6 +90715,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
90052
90715
  access: "view",
90053
90716
  role: "admin"
90054
90717
  },
90718
+ "deviceExport.listSupportedEntityKinds": {
90719
+ capName: "device-export",
90720
+ capScope: "system",
90721
+ addonId: null,
90722
+ access: "view",
90723
+ role: "admin"
90724
+ },
90055
90725
  "deviceExport.unexposeDevice": {
90056
90726
  capName: "device-export",
90057
90727
  capScope: "system",
@@ -90059,6 +90729,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
90059
90729
  access: "create",
90060
90730
  role: "admin"
90061
90731
  },
90732
+ "deviceExport.unexposeEntity": {
90733
+ capName: "device-export",
90734
+ capScope: "system",
90735
+ addonId: null,
90736
+ access: "create",
90737
+ role: "admin"
90738
+ },
90062
90739
  "deviceExtension.listCandidates": {
90063
90740
  capName: "device-extension",
90064
90741
  capScope: "device",
@@ -92880,6 +93557,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
92880
93557
  access: "create",
92881
93558
  role: "admin"
92882
93559
  },
93560
+ "nodeRemovalHook.onNodeRemoved": {
93561
+ capName: "node-removal-hook",
93562
+ capScope: "system",
93563
+ addonId: null,
93564
+ access: "create",
93565
+ role: "admin"
93566
+ },
92883
93567
  "nodes.clusterAddonStatus": {
92884
93568
  capName: "nodes",
92885
93569
  capScope: "system",
@@ -93517,6 +94201,48 @@ var METHOD_ACCESS_MAP = Object.freeze({
93517
94201
  access: "create",
93518
94202
  role: "viewer"
93519
94203
  },
94204
+ "petFountain.getStatus": {
94205
+ capName: "pet-fountain",
94206
+ capScope: "device",
94207
+ addonId: null,
94208
+ access: "view",
94209
+ role: "viewer"
94210
+ },
94211
+ "petFountain.resetDailyConsumption": {
94212
+ capName: "pet-fountain",
94213
+ capScope: "device",
94214
+ addonId: null,
94215
+ access: "delete",
94216
+ role: "viewer"
94217
+ },
94218
+ "petFountain.setPump": {
94219
+ capName: "pet-fountain",
94220
+ capScope: "device",
94221
+ addonId: null,
94222
+ access: "create",
94223
+ role: "viewer"
94224
+ },
94225
+ "petFountain.setPumpInterval": {
94226
+ capName: "pet-fountain",
94227
+ capScope: "device",
94228
+ addonId: null,
94229
+ access: "create",
94230
+ role: "viewer"
94231
+ },
94232
+ "petFountain.setPumpOffMinutes": {
94233
+ capName: "pet-fountain",
94234
+ capScope: "device",
94235
+ addonId: null,
94236
+ access: "create",
94237
+ role: "viewer"
94238
+ },
94239
+ "petFountain.setPumpOnMinutes": {
94240
+ capName: "pet-fountain",
94241
+ capScope: "device",
94242
+ addonId: null,
94243
+ access: "create",
94244
+ role: "viewer"
94245
+ },
93520
94246
  "pipelineAnalytics.addRetrainFrameFromRecording": {
93521
94247
  capName: "pipeline-analytics",
93522
94248
  capScope: "device",
@@ -98115,6 +98841,7 @@ var KNOWN_CAP_NAMES = [
98115
98841
  "network-link",
98116
98842
  "network-quality",
98117
98843
  "node-certificates",
98844
+ "node-removal-hook",
98118
98845
  "nodes",
98119
98846
  "notification-output",
98120
98847
  "notification-rules",
@@ -98125,6 +98852,7 @@ var KNOWN_CAP_NAMES = [
98125
98852
  "osd",
98126
98853
  "osd-manager",
98127
98854
  "pet-feeder",
98855
+ "pet-fountain",
98128
98856
  "pipeline-analytics",
98129
98857
  "pipeline-executor",
98130
98858
  "pipeline-orchestrator",
@@ -98256,6 +98984,7 @@ var DEVICE_CAP_NAMES = [
98256
98984
  "numeric-sensor",
98257
98985
  "osd",
98258
98986
  "pet-feeder",
98987
+ "pet-fountain",
98259
98988
  "pipeline-analytics",
98260
98989
  "power-meter",
98261
98990
  "presence",
@@ -98348,6 +99077,7 @@ var SYSTEM_CAP_NAMES = [
98348
99077
  "network-access",
98349
99078
  "network-quality",
98350
99079
  "node-certificates",
99080
+ "node-removal-hook",
98351
99081
  "nodes",
98352
99082
  "notification-output",
98353
99083
  "notification-rules",
@@ -100394,6 +101124,100 @@ var APPLIANCE_CAP_VOCABULARY = {
100394
101124
  }
100395
101125
  ]
100396
101126
  },
101127
+ "pet-fountain": {
101128
+ scope: "device",
101129
+ noun: t("Pet fountain", "Fontana per animali"),
101130
+ actions: [
101131
+ {
101132
+ id: "petFountain.pumpOn",
101133
+ method: "setPump",
101134
+ label: t("Pump on", "Pompa accesa"),
101135
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
101136
+ icon: "play",
101137
+ args: { on: true }
101138
+ },
101139
+ {
101140
+ id: "petFountain.pumpOff",
101141
+ method: "setPump",
101142
+ label: t("Pump off", "Pompa spenta"),
101143
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
101144
+ icon: "square",
101145
+ args: { on: false }
101146
+ },
101147
+ {
101148
+ id: "petFountain.intervalOn",
101149
+ method: "setPumpInterval",
101150
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
101151
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
101152
+ icon: "timer",
101153
+ args: { on: true }
101154
+ },
101155
+ {
101156
+ id: "petFountain.intervalOff",
101157
+ method: "setPumpInterval",
101158
+ label: t("Pump cycle off", "Ciclo pompa spento"),
101159
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
101160
+ icon: "timer-off",
101161
+ args: { on: false }
101162
+ },
101163
+ {
101164
+ id: "petFountain.pumpOnMinutes",
101165
+ method: "setPumpOnMinutes",
101166
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
101167
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
101168
+ icon: "timer",
101169
+ openArgs: [{
101170
+ key: "minutes",
101171
+ label: t("Minutes", "Minuti"),
101172
+ offer: {
101173
+ kind: "number",
101174
+ min: 0,
101175
+ step: 1,
101176
+ unit: "min"
101177
+ }
101178
+ }]
101179
+ },
101180
+ {
101181
+ id: "petFountain.pumpOffMinutes",
101182
+ method: "setPumpOffMinutes",
101183
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
101184
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
101185
+ icon: "timer-off",
101186
+ openArgs: [{
101187
+ key: "minutes",
101188
+ label: t("Minutes", "Minuti"),
101189
+ offer: {
101190
+ kind: "number",
101191
+ min: 0,
101192
+ step: 1,
101193
+ unit: "min"
101194
+ }
101195
+ }]
101196
+ },
101197
+ {
101198
+ id: "petFountain.resetDailyConsumption",
101199
+ method: "resetDailyConsumption",
101200
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
101201
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
101202
+ icon: "rotate-ccw"
101203
+ }
101204
+ ],
101205
+ state: [{
101206
+ kind: "boolean",
101207
+ field: "empty",
101208
+ label: t("Tank empty", "Serbatoio vuoto"),
101209
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
101210
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
101211
+ icon: "droplet"
101212
+ }, {
101213
+ kind: "numeric",
101214
+ field: "waterLevel",
101215
+ quantity: t("the water level", "il livello dell’acqua"),
101216
+ label: t("Water level", "Livello dell’acqua"),
101217
+ unit: "%",
101218
+ icon: "droplets"
101219
+ }]
101220
+ },
100397
101221
  "media-player": {
100398
101222
  scope: "device",
100399
101223
  noun: t("Media player", "Lettore multimediale"),
@@ -102224,6 +103048,7 @@ var SYSTEM_CAP_VOCABULARY = {
102224
103048
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
102225
103049
  "device-provider": system(INTERNAL),
102226
103050
  "device-removal-hook": system(INTERNAL),
103051
+ "node-removal-hook": system(INTERNAL),
102227
103052
  "device-state": system("the runtime-state mirror every trigger already reads through"),
102228
103053
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
102229
103054
  "embedding-encoder": system(ML),
@@ -102455,6 +103280,7 @@ var DEVICE_TYPE_DO_CAPS = {
102455
103280
  [require_err_msg.DeviceType.Container]: [],
102456
103281
  [require_err_msg.DeviceType.Image]: [],
102457
103282
  [require_err_msg.DeviceType.PetFeeder]: ["pet-feeder"],
103283
+ [require_err_msg.DeviceType.PetFountain]: ["pet-fountain"],
102458
103284
  [require_err_msg.DeviceType.Dishwasher]: ["dishwasher"],
102459
103285
  [require_err_msg.DeviceType.Washer]: [],
102460
103286
  [require_err_msg.DeviceType.Dryer]: []
@@ -104073,6 +104899,7 @@ var CAP_NAMES_WITH_STATUS = [
104073
104899
  "numeric-sensor",
104074
104900
  "osd",
104075
104901
  "pet-feeder",
104902
+ "pet-fountain",
104076
104903
  "power-meter",
104077
104904
  "presence",
104078
104905
  "pressure-sensor",
@@ -104216,6 +105043,7 @@ var CAPABILITY_NAMES = {
104216
105043
  networkLink: "network-link",
104217
105044
  networkQuality: "network-quality",
104218
105045
  nodeCertificates: "node-certificates",
105046
+ nodeRemovalHook: "node-removal-hook",
104219
105047
  nodes: "nodes",
104220
105048
  notificationOutput: "notification-output",
104221
105049
  notificationRules: "notification-rules",
@@ -104226,6 +105054,7 @@ var CAPABILITY_NAMES = {
104226
105054
  osd: "osd",
104227
105055
  osdManager: "osd-manager",
104228
105056
  petFeeder: "pet-feeder",
105057
+ petFountain: "pet-fountain",
104229
105058
  pipelineAnalytics: "pipeline-analytics",
104230
105059
  pipelineExecutor: "pipeline-executor",
104231
105060
  pipelineOrchestrator: "pipeline-orchestrator",
@@ -104754,6 +105583,10 @@ var CAPABILITY_ROUTER_KEYS = [
104754
105583
  key: "nodeCertificates",
104755
105584
  name: "node-certificates"
104756
105585
  },
105586
+ {
105587
+ key: "nodeRemovalHook",
105588
+ name: "node-removal-hook"
105589
+ },
104757
105590
  {
104758
105591
  key: "nodes",
104759
105592
  name: "nodes"
@@ -104794,6 +105627,10 @@ var CAPABILITY_ROUTER_KEYS = [
104794
105627
  key: "petFeeder",
104795
105628
  name: "pet-feeder"
104796
105629
  },
105630
+ {
105631
+ key: "petFountain",
105632
+ name: "pet-fountain"
105633
+ },
104797
105634
  {
104798
105635
  key: "pipelineAnalytics",
104799
105636
  name: "pipeline-analytics"
@@ -105069,7 +105906,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
105069
105906
  actionSourceCapability,
105070
105907
  actionsCapability,
105071
105908
  addonPagesCapability,
105072
- addonPagesSourceCapability,
105909
+ require_sleep.addonPagesSourceCapability,
105073
105910
  addonRoutesCapability,
105074
105911
  addonSettingsCapability,
105075
105912
  addonWidgetsCapability,
@@ -105180,6 +106017,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
105180
106017
  networkLinkCapability,
105181
106018
  networkQualityCapability,
105182
106019
  nodeCertificatesCapability,
106020
+ nodeRemovalHookCapability,
105183
106021
  nodesCapability,
105184
106022
  notificationOutputCapability,
105185
106023
  notificationRulesCapability,
@@ -105190,6 +106028,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
105190
106028
  osdCapability,
105191
106029
  osdManagerCapability,
105192
106030
  petFeederCapability,
106031
+ petFountainCapability,
105193
106032
  pipelineAnalyticsCapability,
105194
106033
  pipelineExecutorCapability,
105195
106034
  pipelineOrchestratorCapability,
@@ -106768,6 +107607,36 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
106768
107607
  form: "single",
106769
107608
  optional: false
106770
107609
  }],
107610
+ "petFountain.getStatus": [{
107611
+ name: "deviceId",
107612
+ form: "single",
107613
+ optional: false
107614
+ }],
107615
+ "petFountain.resetDailyConsumption": [{
107616
+ name: "deviceId",
107617
+ form: "single",
107618
+ optional: false
107619
+ }],
107620
+ "petFountain.setPump": [{
107621
+ name: "deviceId",
107622
+ form: "single",
107623
+ optional: false
107624
+ }],
107625
+ "petFountain.setPumpInterval": [{
107626
+ name: "deviceId",
107627
+ form: "single",
107628
+ optional: false
107629
+ }],
107630
+ "petFountain.setPumpOffMinutes": [{
107631
+ name: "deviceId",
107632
+ form: "single",
107633
+ optional: false
107634
+ }],
107635
+ "petFountain.setPumpOnMinutes": [{
107636
+ name: "deviceId",
107637
+ form: "single",
107638
+ optional: false
107639
+ }],
106771
107640
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
106772
107641
  name: "deviceId",
106773
107642
  form: "single",
@@ -108325,7 +109194,7 @@ function getCapsByProviderKind(kind) {
108325
109194
  //#region src/generated/runtime-state-durability.ts
108326
109195
  /**
108327
109196
  * Per-cap runtime-state policy, projected from every `*.cap.ts` that declares
108328
- * `runtimeState`. 76 caps: 39 `restored`, 37 `session`.
109197
+ * `runtimeState`. 77 caps: 39 `restored`, 38 `session`.
108329
109198
  *
108330
109199
  * A cap absent from this map has no runtime-state slice at all. A cap PRESENT
108331
109200
  * with `durability: 'session'` has one and has decided not to keep it.
@@ -108531,6 +109400,10 @@ var RUNTIME_STATE_POLICY = {
108531
109400
  durability: "session",
108532
109401
  volatileFields: []
108533
109402
  },
109403
+ "pet-fountain": {
109404
+ durability: "session",
109405
+ volatileFields: []
109406
+ },
108534
109407
  "power-meter": {
108535
109408
  durability: "restored",
108536
109409
  volatileFields: ["lastFetchedAt"]
@@ -109518,7 +110391,11 @@ function createSystemProxy(api) {
109518
110391
  listSupportedDeviceKinds: (input) => dispatch("deviceExport", "listSupportedDeviceKinds", "query", input),
109519
110392
  listExposedDevices: (input) => dispatch("deviceExport", "listExposedDevices", "query", input),
109520
110393
  exposeDevice: (input) => dispatch("deviceExport", "exposeDevice", "mutation", input),
109521
- unexposeDevice: (input) => dispatch("deviceExport", "unexposeDevice", "mutation", input)
110394
+ unexposeDevice: (input) => dispatch("deviceExport", "unexposeDevice", "mutation", input),
110395
+ listSupportedEntityKinds: (input) => dispatch("deviceExport", "listSupportedEntityKinds", "query", input),
110396
+ listExposedEntities: (input) => dispatch("deviceExport", "listExposedEntities", "query", input),
110397
+ exposeEntity: (input) => dispatch("deviceExport", "exposeEntity", "mutation", input),
110398
+ unexposeEntity: (input) => dispatch("deviceExport", "unexposeEntity", "mutation", input)
109522
110399
  },
109523
110400
  deviceHistory: {
109524
110401
  listNodeSeries: (input) => dispatch("deviceHistory", "listNodeSeries", "query", input),
@@ -119078,6 +119955,7 @@ var OAUTH_LANDING_REASONS = [
119078
119955
  "unknown_broker",
119079
119956
  "not_configured",
119080
119957
  "exchange_failed",
119958
+ "no_authorization_code",
119081
119959
  "no_refresh_token",
119082
119960
  "unavailable",
119083
119961
  "unknown"
@@ -119305,6 +120183,83 @@ function filterForwardedCookies(cookieHeader) {
119305
120183
  return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
119306
120184
  }
119307
120185
  //#endregion
120186
+ //#region src/vendor-login-proxy/flow-trace.ts
120187
+ /** A short stable label for the attempt: enough to group lines, useless as a capability. */
120188
+ function attemptLabel(attemptId) {
120189
+ let hash = 2166136261;
120190
+ for (let i = 0; i < attemptId.length; i += 1) hash = Math.imul(hash ^ attemptId.charCodeAt(i), 16777619) >>> 0;
120191
+ return hash.toString(16).padStart(8, "0");
120192
+ }
120193
+ /** Distinct parameter names of a `?a=1&b=2` string (or of a urlencoded body), in order. */
120194
+ function paramNames(query) {
120195
+ const names = [];
120196
+ const raw = query.startsWith("?") ? query.slice(1) : query;
120197
+ for (const pair of raw.split("&")) {
120198
+ if (pair.length === 0) continue;
120199
+ const eq = pair.indexOf("=");
120200
+ const rawName = eq === -1 ? pair : pair.slice(0, eq);
120201
+ let name;
120202
+ try {
120203
+ name = decodeURIComponent(rawName.replace(/\+/g, " "));
120204
+ } catch {
120205
+ name = rawName;
120206
+ }
120207
+ if (!names.includes(name)) names.push(name);
120208
+ }
120209
+ return names;
120210
+ }
120211
+ function hasOa2(names) {
120212
+ return names.some((name) => name.startsWith("openid.oa2.") || name === "openid.ns.oa2");
120213
+ }
120214
+ function cookieNamesOf(setCookies) {
120215
+ return setCookies.map((c) => c.split(";")[0]?.split("=")[0]?.trim() ?? "");
120216
+ }
120217
+ function mediaType(contentType) {
120218
+ return (contentType ?? "none").split(";")[0]?.trim().toLowerCase() || "none";
120219
+ }
120220
+ /** Host and path of a Location (no query), or 'unparsable'. */
120221
+ function locationShape(location, base) {
120222
+ try {
120223
+ const url = new URL(location, base);
120224
+ return {
120225
+ target: `${url.host}${url.pathname}`,
120226
+ names: paramNames(url.search)
120227
+ };
120228
+ } catch {
120229
+ return {
120230
+ target: "unparsable",
120231
+ names: []
120232
+ };
120233
+ }
120234
+ }
120235
+ /** The `meta` of one trace line. Values: never. */
120236
+ function flowTraceMeta(input) {
120237
+ const queryNames = paramNames(input.requestSearch);
120238
+ const formNames = input.formBody === null ? null : paramNames(input.formBody);
120239
+ const meta = {
120240
+ session: attemptLabel(input.attemptId),
120241
+ method: input.method,
120242
+ upstream: `${input.upstreamHost}${input.upstreamPath}`,
120243
+ queryNames: queryNames.join(",") || "none",
120244
+ oa2InQuery: hasOa2(queryNames),
120245
+ status: input.status,
120246
+ setCookieNames: cookieNamesOf(input.setCookies).join(",") || "none",
120247
+ cookiesSent: (input.cookieHeader ?? "").split(";").filter((p) => p.trim().length > 0).length
120248
+ };
120249
+ if (formNames !== null) {
120250
+ meta["contentType"] = mediaType(input.contentType);
120251
+ meta["formNames"] = formNames.join(",") || "none";
120252
+ meta["oa2InForm"] = hasOa2(formNames);
120253
+ }
120254
+ if (input.location !== null) {
120255
+ const loc = locationShape(input.location, `http://${input.upstreamHost}/`);
120256
+ meta["location"] = loc.target;
120257
+ meta["locationQueryNames"] = loc.names.join(",") || "none";
120258
+ meta["oa2InLocation"] = hasOa2(loc.names);
120259
+ }
120260
+ return meta;
120261
+ }
120262
+ //#endregion
119308
120263
  //#region src/vendor-login-proxy/login-proxy.ts
119309
120264
  var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
119310
120265
  var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
@@ -119410,6 +120365,8 @@ var VendorLoginProxy = class {
119410
120365
  #now;
119411
120366
  #fetch;
119412
120367
  #attempts = /* @__PURE__ */ new Map();
120368
+ /** Trace lines written per attempt (bounded by FLOW_TRACE_MAX_LINES). */
120369
+ #traced = /* @__PURE__ */ new Map();
119413
120370
  constructor(options) {
119414
120371
  this.#addonId = options.addonId;
119415
120372
  this.#logger = options.logger;
@@ -119436,6 +120393,7 @@ var VendorLoginProxy = class {
119436
120393
  }
119437
120394
  close(attemptId) {
119438
120395
  this.#attempts.delete(attemptId);
120396
+ this.#traced.delete(attemptId);
119439
120397
  }
119440
120398
  isOpen(attemptId) {
119441
120399
  const attempt = this.#attempts.get(attemptId);
@@ -119482,7 +120440,7 @@ var VendorLoginProxy = class {
119482
120440
  const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
119483
120441
  const attempt = this.#attempts.get(parsed.attemptId);
119484
120442
  if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
119485
- if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
120443
+ if (attempt !== void 0) this.close(parsed.attemptId);
119486
120444
  this.#logger.info("vendor login proxy: attempt refused", { meta: {
119487
120445
  addonId: this.#addonId,
119488
120446
  reason: attempt === void 0 ? "missing" : "expired"
@@ -119507,13 +120465,17 @@ var VendorLoginProxy = class {
119507
120465
  }
119508
120466
  const method = (req.method ?? "GET").toUpperCase();
119509
120467
  let body;
120468
+ let formBodyRaw = null;
119510
120469
  if (method !== "GET" && method !== "HEAD") {
119511
120470
  const read = await readRequestBody(req);
119512
120471
  if (read === "too-large") {
119513
120472
  sendPlain(res, 413, "Request too large");
119514
120473
  return;
119515
120474
  }
119516
- body = isFormEncoded(req.headers["content-type"]) ? Buffer.from(rewriteFormBody(read.toString("utf8"), route), "utf8") : read;
120475
+ if (isFormEncoded(req.headers["content-type"])) {
120476
+ formBodyRaw = read.toString("utf8");
120477
+ body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
120478
+ } else body = read;
119517
120479
  }
119518
120480
  const headers = this.#requestHeaders(req, route, upstream);
119519
120481
  const upstreamRes = await this.#fetch(target, {
@@ -119538,6 +120500,19 @@ var VendorLoginProxy = class {
119538
120500
  sentNames: cookieNames(headers["cookie"]),
119539
120501
  vendorSets: setCookies.map(setCookieShape).join(" | ")
119540
120502
  } });
120503
+ this.#trace(parsed.attemptId, {
120504
+ attemptId: parsed.attemptId,
120505
+ method,
120506
+ upstreamHost: upstream.host,
120507
+ upstreamPath: target.pathname,
120508
+ requestSearch: target.search,
120509
+ formBody: formBodyRaw,
120510
+ contentType: firstHeader(req.headers["content-type"]),
120511
+ status: upstreamRes.status,
120512
+ location,
120513
+ setCookies,
120514
+ cookieHeader: headers["cookie"]
120515
+ });
119541
120516
  const settled = await attempt.evaluate({
119542
120517
  status: upstreamRes.status,
119543
120518
  url: target.toString(),
@@ -119546,7 +120521,7 @@ var VendorLoginProxy = class {
119546
120521
  bodyText: text
119547
120522
  });
119548
120523
  if (settled.kind === "done") {
119549
- this.#attempts.delete(parsed.attemptId);
120524
+ this.close(parsed.attemptId);
119550
120525
  this.#logger.info("vendor login proxy: sign-in finished", { meta: {
119551
120526
  addonId: this.#addonId,
119552
120527
  status: settled.landing.status
@@ -119570,6 +120545,16 @@ var VendorLoginProxy = class {
119570
120545
  res.writeHead(upstreamRes.status, out);
119571
120546
  await this.#pipeBody(upstreamRes, res);
119572
120547
  }
120548
+ /** One names-only line per proxied request, at most FLOW_TRACE_MAX_LINES per attempt. */
120549
+ #trace(attemptId, input) {
120550
+ const written = this.#traced.get(attemptId) ?? 0;
120551
+ if (written >= 200) return;
120552
+ this.#traced.set(attemptId, written + 1);
120553
+ this.#logger.info("vendor login proxy: flow", { meta: {
120554
+ addonId: this.#addonId,
120555
+ ...flowTraceMeta(input)
120556
+ } });
120557
+ }
119573
120558
  #requestHeaders(req, route, upstream) {
119574
120559
  const headers = {};
119575
120560
  for (const [name, value] of Object.entries(req.headers)) {
@@ -120833,7 +121818,7 @@ exports.AddBrokerInputSchema = AddBrokerInputSchema;
120833
121818
  exports.AddRetrainFrameFromRecordingResultSchema = AddRetrainFrameFromRecordingResultSchema;
120834
121819
  exports.AddonAutoUpdateSchema = AddonAutoUpdateSchema;
120835
121820
  exports.AddonListItemSchema = AddonListItemSchema;
120836
- exports.AddonPageDeclarationSchema = AddonPageDeclarationSchema;
121821
+ exports.AddonPageDeclarationSchema = require_sleep.AddonPageDeclarationSchema;
120837
121822
  exports.AddonPageInfoSchema = AddonPageInfoSchema;
120838
121823
  exports.AddonRoutesScopeSchema = AddonRoutesScopeSchema;
120839
121824
  exports.AdminLinkTargetEnum = AdminLinkTargetEnum;
@@ -121164,6 +122149,7 @@ exports.CompositionFieldSourceSchema = require_composition.CompositionFieldSourc
121164
122149
  exports.CompositionFieldStateKindSchema = CompositionFieldStateKindSchema;
121165
122150
  exports.CompositionFieldStateSchema = CompositionFieldStateSchema;
121166
122151
  exports.CompositionFieldsFeatureSchema = require_composition.CompositionFieldsFeatureSchema;
122152
+ exports.CompositionForwardArgSchema = require_composition.CompositionForwardArgSchema;
121167
122153
  exports.CompositionForwardCommandSchema = require_composition.CompositionForwardCommandSchema;
121168
122154
  exports.CompositionFromSourceSchema = require_composition.CompositionFromSourceSchema;
121169
122155
  exports.CompositionGetReadSchema = CompositionGetReadSchema;
@@ -121290,6 +122276,7 @@ exports.DEVICE_HISTORY_ENERGY_KEY = DEVICE_HISTORY_ENERGY_KEY;
121290
122276
  exports.DEVICE_HISTORY_ORPHAN_DELETE_MAX = DEVICE_HISTORY_ORPHAN_DELETE_MAX;
121291
122277
  exports.DEVICE_HISTORY_ORPHAN_LIST_MAX = DEVICE_HISTORY_ORPHAN_LIST_MAX;
121292
122278
  exports.DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS = DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS;
122279
+ exports.DEVICE_ID_FIELD = DEVICE_ID_FIELD;
121293
122280
  exports.DEVICE_NONCE_TTL_MS = DEVICE_NONCE_TTL_MS;
121294
122281
  exports.DEVICE_PROFILES = DEVICE_PROFILES;
121295
122282
  exports.DEVICE_RAMP = DEVICE_RAMP;
@@ -121472,6 +122459,8 @@ exports.ExportClipSubjectSchema = ExportClipSubjectSchema;
121472
122459
  exports.ExportDenseRangeSchema = ExportDenseRangeSchema;
121473
122460
  exports.ExportDenseSchema = ExportDenseSchema;
121474
122461
  exports.ExportDownloadSchema = ExportDownloadSchema;
122462
+ exports.ExportEntityKindSchema = ExportEntityKindSchema;
122463
+ exports.ExportEntityRefSchema = ExportEntityRefSchema;
121475
122464
  exports.ExportFootageSubjectSchema = ExportFootageSubjectSchema;
121476
122465
  exports.ExportOptionsSchema = ExportOptionsSchema;
121477
122466
  exports.ExportRecordSchema = ExportRecordSchema;
@@ -121481,7 +122470,9 @@ exports.ExportSpeedSchema = ExportSpeedSchema;
121481
122470
  exports.ExportStateSchema = ExportStateSchema;
121482
122471
  exports.ExportSubjectSchema = ExportSubjectSchema;
121483
122472
  exports.ExportTimelapseSchema = ExportTimelapseSchema;
122473
+ exports.ExposeEntityInputSchema = ExposeEntityInputSchema;
121484
122474
  exports.ExposedDeviceSchema = ExposedDeviceSchema;
122475
+ exports.ExposedEntitySchema = ExposedEntitySchema;
121485
122476
  exports.ExposureModeSchema = ExposureModeSchema;
121486
122477
  exports.ExpressionBindingSourceSchema = ExpressionBindingSourceSchema;
121487
122478
  exports.ExpressionEvalError = require_composition.ExpressionEvalError;
@@ -121517,6 +122508,7 @@ exports.FEEDER_EXAMPLE_ADDON_ID = FEEDER_EXAMPLE_ADDON_ID;
121517
122508
  exports.FEEDER_EXAMPLE_HA_DEVICE_ID = FEEDER_EXAMPLE_HA_DEVICE_ID;
121518
122509
  exports.FFMPEG_PATH_ENV = require_composition.FFMPEG_PATH_ENV;
121519
122510
  exports.FIRST_LEVEL_MACRO_CLASSES = FIRST_LEVEL_MACRO_CLASSES;
122511
+ exports.FORWARD_INPUT_KEY = require_composition.FORWARD_INPUT_KEY;
121520
122512
  exports.FULL_IMAGE_BBOX = FULL_IMAGE_BBOX;
121521
122513
  exports.FailureContributionSchema = FailureContributionSchema;
121522
122514
  exports.FailureCounters = FailureCounters;
@@ -122125,6 +123117,8 @@ exports.NodeDirectoryEntrySchema = NodeDirectoryEntrySchema;
122125
123117
  exports.NodeDirectorySchema = NodeDirectorySchema;
122126
123118
  exports.NodeHttpsStateSchema = NodeHttpsStateSchema;
122127
123119
  exports.NodeRecommendationSchema = NodeRecommendationSchema;
123120
+ exports.NodeRemovedInputSchema = NodeRemovedInputSchema;
123121
+ exports.NodeRemovedResultSchema = NodeRemovedResultSchema;
122128
123122
  exports.NotificationActionIconSchema = NotificationActionIconSchema;
122129
123123
  exports.NotificationActionSchema = NotificationActionSchema;
122130
123124
  exports.NotificationFormatSchema = NotificationFormatSchema;
@@ -122227,6 +123221,7 @@ exports.PcmSampleFormatSchema = PcmSampleFormatSchema;
122227
123221
  exports.PeerBytesTicketSchema = require_sleep.PeerBytesTicketSchema;
122228
123222
  exports.PerScopeBreakdownSchema = PerScopeBreakdownSchema;
122229
123223
  exports.PetFeederStatusSchema = PetFeederStatusSchema;
123224
+ exports.PetFountainStatusSchema = PetFountainStatusSchema;
122230
123225
  exports.PickStreamPreferencesSchema = PickStreamPreferencesSchema;
122231
123226
  exports.PickStreamRequirementsSchema = PickStreamRequirementsSchema;
122232
123227
  exports.PickedCamStreamSchema = PickedCamStreamSchema;
@@ -122234,8 +123229,8 @@ exports.PipelineAssignmentSchema = PipelineAssignmentSchema;
122234
123229
  exports.PipelineDefaultStepSchema = PipelineDefaultStepSchema;
122235
123230
  exports.PipelineEngineChoiceSchema = PipelineEngineChoiceSchema;
122236
123231
  exports.PipelineModelFormatStatusSchema = PipelineModelFormatStatusSchema;
122237
- exports.PipelineRunResultBridge = PipelineRunResultBridge;
122238
- exports.PipelineStepInputSchema = PipelineStepInputSchema;
123232
+ exports.PipelineRunResultBridge = require_sleep.PipelineRunResultBridge;
123233
+ exports.PipelineStepInputSchema = require_sleep.PipelineStepInputSchema;
122239
123234
  exports.PipelineValidationIssueSchema = PipelineValidationIssueSchema;
122240
123235
  exports.PipelineValidationResultSchema = PipelineValidationResultSchema;
122241
123236
  exports.PlaceholderReasonSchema = PlaceholderReasonSchema;
@@ -122824,6 +123819,7 @@ exports.UNIT_TABLE = UNIT_TABLE;
122824
123819
  exports.UNLOCK_CONFIRMATION = UNLOCK_CONFIRMATION;
122825
123820
  exports.USE_ROOT_MODEL_DEFAULTS = USE_ROOT_MODEL_DEFAULTS;
122826
123821
  exports.USE_ROOT_MODEL_SETTING_KEY = USE_ROOT_MODEL_SETTING_KEY;
123822
+ exports.UnexposeEntityInputSchema = UnexposeEntityInputSchema;
122827
123823
  exports.UnifiedBrokerInfoSchema = BrokerInfoSchema;
122828
123824
  exports.UnindexedFilterError = require_sleep.UnindexedFilterError;
122829
123825
  exports.UninstallNodeOutcomeSchema = UninstallNodeOutcomeSchema;
@@ -122928,7 +123924,7 @@ exports.actuatorMethodRow = actuatorMethodRow;
122928
123924
  exports.addonCatalogHref = addonCatalogHref;
122929
123925
  exports.addonIdOfActionId = addonIdOfActionId;
122930
123926
  exports.addonPagesCapability = addonPagesCapability;
122931
- exports.addonPagesSourceCapability = addonPagesSourceCapability;
123927
+ exports.addonPagesSourceCapability = require_sleep.addonPagesSourceCapability;
122932
123928
  exports.addonRoutesCapability = addonRoutesCapability;
122933
123929
  exports.addonSettingsCapability = addonSettingsCapability;
122934
123930
  exports.addonWidgetsCapability = addonWidgetsCapability;
@@ -123084,6 +124080,8 @@ exports.collectSecretMarkerRefs = require_sleep.collectSecretMarkerRefs;
123084
124080
  exports.collectZodSecretPaths = require_sleep.collectZodSecretPaths;
123085
124081
  exports.colorCapability = colorCapability;
123086
124082
  exports.colorForKind = colorForKind;
124083
+ exports.commandMethodProblems = commandMethodProblems;
124084
+ exports.commandedMethods = commandedMethods;
123087
124085
  exports.commitWatchdogRestart = commitWatchdogRestart;
123088
124086
  exports.compareToTarget = compareToTarget;
123089
124087
  exports.compileExpression = require_composition.compileExpression;
@@ -123297,6 +124295,7 @@ exports.failureRate = failureRate;
123297
124295
  exports.fanControlCapability = fanControlCapability;
123298
124296
  exports.fanOutTargets = fanOutTargets;
123299
124297
  exports.featureFieldEntries = require_composition.featureFieldEntries;
124298
+ exports.featureForwards = featureForwards;
123300
124299
  exports.featureOfRole = featureOfRole;
123301
124300
  exports.featureProbeCapability = featureProbeCapability;
123302
124301
  exports.featureSources = require_composition.featureSources;
@@ -123324,6 +124323,9 @@ exports.formatParkedFor = formatParkedFor;
123324
124323
  exports.formatReading = formatReading;
123325
124324
  exports.formatSecretScope = require_sleep.formatSecretScope;
123326
124325
  exports.formulaProblem = formulaProblem;
124326
+ exports.forwardProblems = forwardProblems;
124327
+ exports.forwardSourceProblems = forwardSourceProblems;
124328
+ exports.forwardSourceRefs = forwardSourceRefs;
123327
124329
  exports.freshResults = freshResults;
123328
124330
  exports.fuseRecoveredDetections = fuseRecoveredDetections;
123329
124331
  exports.gasCapability = gasCapability;
@@ -123414,6 +124416,7 @@ exports.isEvent = require_sleep.isEvent;
123414
124416
  exports.isEventKindLabelKey = isEventKindLabelKey;
123415
124417
  exports.isEventOwnerType = isEventOwnerType;
123416
124418
  exports.isFirstLevelMacroClass = isFirstLevelMacroClass;
124419
+ exports.isForwardInputRef = require_composition.isForwardInputRef;
123417
124420
  exports.isHeldBackTemplateVar = isHeldBackTemplateVar;
123418
124421
  exports.isHistoryHelper = require_composition.isHistoryHelper;
123419
124422
  exports.isIconKey = isIconKey;
@@ -123559,6 +124562,7 @@ exports.nodeBenchmarkRefusals = nodeBenchmarkRefusals;
123559
124562
  exports.nodeCertificatesCapability = nodeCertificatesCapability;
123560
124563
  exports.nodePickerItems = nodePickerItems;
123561
124564
  exports.nodePin = require_sleep.nodePin;
124565
+ exports.nodeRemovalHookCapability = nodeRemovalHookCapability;
123562
124566
  exports.nodesCapability = nodesCapability;
123563
124567
  exports.normalizeAddonInitResult = require_sleep.normalizeAddonInitResult;
123564
124568
  exports.normalizeAudioLabel = normalizeAudioLabel;
@@ -123628,6 +124632,8 @@ exports.parseStreamParamsFormPatch = parseStreamParamsFormPatch;
123628
124632
  exports.parseTrainingEventLine = parseTrainingEventLine;
123629
124633
  exports.patchAudio = patchAudio;
123630
124634
  exports.petFeederCapability = petFeederCapability;
124635
+ exports.petFountainCapability = petFountainCapability;
124636
+ exports.petFountainFromEntitiesComposition = petFountainFromEntitiesComposition;
123631
124637
  exports.pickAccessoryControl = pickAccessoryControl;
123632
124638
  exports.pickAppLinkHub = pickAppLinkHub;
123633
124639
  exports.pickClusterStepModels = pickClusterStepModels;
@@ -123676,6 +124682,7 @@ exports.projectScalarAssignment = projectScalarAssignment;
123676
124682
  exports.projectSpanAssignment = projectSpanAssignment;
123677
124683
  exports.projectSubjectPath = projectSubjectPath;
123678
124684
  exports.providerMotionEvent = providerMotionEvent;
124685
+ exports.pruneDeletedEntities = pruneDeletedEntities;
123679
124686
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
123680
124687
  exports.ptzCapability = ptzCapability;
123681
124688
  exports.publicEventCategories = require_event_category.publicEventCategories;
@@ -123805,6 +124812,7 @@ exports.ruleSection = ruleSection;
123805
124812
  exports.ruleSectionOf = ruleSectionOf;
123806
124813
  exports.ruleSeedForSection = ruleSeedForSection;
123807
124814
  exports.ruleTriggerZoneIds = ruleTriggerZoneIds;
124815
+ exports.runExportedAction = runExportedAction;
123808
124816
  exports.runFanOut = runFanOut;
123809
124817
  exports.runInferenceStep = runInferenceStep;
123810
124818
  exports.runSteps = runSteps;
@@ -123843,6 +124851,7 @@ exports.selectAssignedProfileSlots = require_sleep.selectAssignedProfileSlots;
123843
124851
  exports.selectRecoveryStep = selectRecoveryStep;
123844
124852
  exports.sendTimesForEditor = sendTimesForEditor;
123845
124853
  exports.sendTimesOfSchedule = sendTimesOfSchedule;
124854
+ exports.sendToExportedTarget = sendToExportedTarget;
123846
124855
  exports.sensorRaisedOf = sensorRaisedOf;
123847
124856
  exports.sensorTransitionNeverFires = sensorTransitionNeverFires;
123848
124857
  exports.sensorTransitionOffered = sensorTransitionOffered;