@camstack/system 1.2.61 → 1.2.63

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 (86) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/block-package.d.ts +9 -27
  14. package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
  17. package/dist/builtins/device-manager/device-aggregation.d.ts +49 -2
  18. package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
  19. package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
  20. package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
  21. package/dist/builtins/device-manager/device-manager.addon.js +182 -888
  22. package/dist/builtins/device-manager/device-manager.addon.mjs +182 -888
  23. package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
  24. package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
  25. package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
  26. package/dist/builtins/device-manager/device-projection.d.ts +1 -2
  27. package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
  28. package/dist/builtins/device-manager/device-queries.d.ts +1 -1
  29. package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
  30. package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
  31. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  32. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  33. package/dist/builtins/hub-forwarder/index.js +1 -1
  34. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  37. package/dist/builtins/local-auth/homeassistant-integration.d.ts +7 -0
  38. package/dist/builtins/local-auth/index.d.ts +1 -0
  39. package/dist/builtins/local-auth/index.js +1 -0
  40. package/dist/builtins/local-auth/index.mjs +2 -2
  41. package/dist/builtins/local-auth/local-auth.addon.js +561 -497
  42. package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
  43. package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
  44. package/dist/builtins/local-auth/oauth-session-manager.d.ts +1 -1
  45. package/dist/builtins/local-network/local-network.addon.js +1 -1
  46. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  47. package/dist/builtins/loki-logging/index.js +1 -1
  48. package/dist/builtins/loki-logging/index.mjs +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  50. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  51. package/dist/builtins/platform-probe/index.js +1 -1
  52. package/dist/builtins/platform-probe/index.mjs +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  54. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/snapshot/index.js +319 -20
  56. package/dist/builtins/snapshot/index.mjs +319 -20
  57. package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
  58. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
  59. package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -1
  60. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  61. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  62. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +15 -0
  63. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +45 -5
  64. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +45 -5
  65. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  66. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  67. package/dist/builtins/system-config/system-config.addon.js +1 -1
  68. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  69. package/dist/builtins/winston-logging/index.js +1 -1
  70. package/dist/builtins/winston-logging/index.mjs +1 -1
  71. package/dist/{dist-L6QPJWsr.mjs → dist-BJ2w3uoN.mjs} +1307 -1313
  72. package/dist/{dist-BN681PgO.js → dist-LDrOFrd-.js} +1255 -1273
  73. package/dist/index.d.ts +1 -1
  74. package/dist/index.js +209 -5
  75. package/dist/index.mjs +206 -7
  76. package/dist/kernel/addon-residue.d.ts +49 -0
  77. package/dist/kernel/index.d.ts +2 -0
  78. package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
  79. package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-CopL1_5C.mjs} +20 -14
  80. package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-DNfmBu20.js} +20 -14
  81. package/package.json +5 -1
  82. package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
  83. package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
  84. package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
  85. package/dist/main-CVLq8IzA.js +0 -2356
  86. package/dist/main-CzH19GkU.mjs +0 -2353
@@ -5434,6 +5434,69 @@ var StreamFormatSchema = z.enum([
5434
5434
  "mjpeg",
5435
5435
  "rtsp"
5436
5436
  ]);
5437
+ /** A container `produceEventMedia` can emit. */
5438
+ var EventMediaKindSchema = z.enum(["mp4", "gif"]);
5439
+ /**
5440
+ * One produced artifact, referenced by HANDLE.
5441
+ *
5442
+ * Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
5443
+ * method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
5444
+ * [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
5445
+ * on demand, compressed, by handle). `bytes` is here so a caller can decide
5446
+ * whether it wants the fetch at all.
5447
+ */
5448
+ var EventMediaArtifactSchema = z.object({
5449
+ kind: EventMediaKindSchema,
5450
+ /** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
5451
+ handle: z.string(),
5452
+ /**
5453
+ * The node holding the bytes — the ROUTING key for `fetchEventMedia`.
5454
+ *
5455
+ * `stream-broker` is a singleton cap and an unpinned call never leaves the
5456
+ * hub, so a handle produced on an agent's broker would be redeemed against
5457
+ * the hub's store and come back `null`. Same contract, same field name and
5458
+ * the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
5459
+ * lives and the consumer pins to it.
5460
+ */
5461
+ nodeId: z.string(),
5462
+ mime: z.string(),
5463
+ bytes: z.number().int(),
5464
+ width: z.number().int(),
5465
+ height: z.number().int()
5466
+ });
5467
+ /**
5468
+ * What a production actually covered — the answer to the only question an
5469
+ * operator asks about a notification clip.
5470
+ *
5471
+ * `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
5472
+ * so a caller can state "this clip starts 4.1 s before the event" instead of
5473
+ * inferring it from a duration. A production whose `fromTs` is later than the
5474
+ * event is a production with no pre-roll, and that is exactly the defect this
5475
+ * method exists to make visible rather than plausible.
5476
+ */
5477
+ var EventMediaCoverageSchema = z.object({
5478
+ fromTs: z.number(),
5479
+ toTs: z.number(),
5480
+ /** Encoded packets in the muxed window. */
5481
+ packets: z.number().int()
5482
+ });
5483
+ /**
5484
+ * The result of ONE cut, in every container the caller asked for.
5485
+ *
5486
+ * Every artifact in `media` came out of the SAME window of the SAME rendition —
5487
+ * that is the whole reason this is one method rather than one call per format.
5488
+ * A consumer attaching a gif and a video can no longer show two different
5489
+ * moments, because it never chose two sources.
5490
+ */
5491
+ var EventMediaProductionSchema = z.object({
5492
+ media: z.array(EventMediaArtifactSchema).readonly(),
5493
+ coverage: EventMediaCoverageSchema,
5494
+ /** The rendition actually cut from — what the default or the fallback chose. */
5495
+ profile: CamProfileSchema,
5496
+ /** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
5497
+ * source, a downscale, or a playback rate other than 1). */
5498
+ video: z.enum(["copy", "encode"])
5499
+ });
5437
5500
  var RtspRestreamEntrySchema = z.object({
5438
5501
  brokerId: z.string(),
5439
5502
  url: z.string(),
@@ -5871,6 +5934,88 @@ var streamBrokerCapability = {
5871
5934
  kind: "mutation",
5872
5935
  auth: "admin"
5873
5936
  }),
5937
+ /**
5938
+ * THE production point for event media — one window, every container.
5939
+ *
5940
+ * `renderPreBufferClip` renders ONE format per call, so a consumer wanting a
5941
+ * gif and a video made two calls and got two windows. On 2026-08-08 the
5942
+ * operator received exactly that: a gif around a doorbell press and an mp4
5943
+ * of the six seconds after it. This method cuts once and derives the rest,
5944
+ * so "the attachments agree" is a property of the service instead of a
5945
+ * discipline every consumer has to keep.
5946
+ *
5947
+ * It is also the only place that owns the packets. The broker already
5948
+ * retains an AnnexB ring per camera (`setClipRetention`, D32) — the
5949
+ * prebuffer — so a production reaches BACKWARDS from the event with no
5950
+ * process to spawn and nothing kept warm. It waits out `postSeconds`, muxes
5951
+ * the window once (COPYING the camera's own H.264 whenever nothing is being
5952
+ * changed), derives any gif from that same mux, and returns HANDLES.
5953
+ *
5954
+ * Intended for every future consumer, not just notifications: a clips API, a
5955
+ * retrain export and an event export all want the same window of the same
5956
+ * camera and must not each grow their own renderer.
5957
+ */
5958
+ produceEventMedia: method(z.object({
5959
+ deviceId: z.number(),
5960
+ /** Absent = the largest H.264 rendition at or below 1080p, which is
5961
+ * also the one that can be copied. Falls back to whatever the ring
5962
+ * actually retained, and the answer says which. */
5963
+ profile: CamProfileSchema.optional(),
5964
+ aroundMs: z.number(),
5965
+ preSeconds: z.number().min(0).max(20).default(4),
5966
+ postSeconds: z.number().min(0).max(20).default(6),
5967
+ kinds: z.array(EventMediaKindSchema).min(1).default(["mp4"]),
5968
+ /** GIF geometry. The video keeps the source's own. */
5969
+ gifMaxWidth: z.number().int().min(120).max(1280).default(640),
5970
+ /**
5971
+ * The gif's own PLAYBACK rate in frames per second — what the finished
5972
+ * gif runs at, not how many source frames feed it. The decimation that
5973
+ * feeds it samples `gifFps / gifSpeed` source frames per second, so at
5974
+ * the defaults a 12 fps gif is built out of 3 source frames a second.
5975
+ */
5976
+ gifFps: z.number().int().min(1).max(15).default(12),
5977
+ /**
5978
+ * How fast the GIF plays against real time, independent of `speed`.
5979
+ *
5980
+ * 4× by default, by operator request: a notification gif is glanced at
5981
+ * on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
5982
+ * a separate knob from `speed` even though both now default to 4 —
5983
+ * a caller wanting a real-time video and a fast gif must not have to
5984
+ * choose.
5985
+ */
5986
+ gifSpeed: z.number().min(1).max(8).default(4),
5987
+ /**
5988
+ * Playback rate of the VIDEO. Also 4× by default, by operator decision.
5989
+ *
5990
+ * `1` is real time and is the ONLY value that allows the copy branch —
5991
+ * anything else forces `libx264` over the window. That was priced
5992
+ * before it was chosen: a per-event burst measured at 0.23 s and 254 KB
5993
+ * on a real 615 720p cut, against 922 KB for the copy it replaces. A
5994
+ * re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
5995
+ * once the decode is forced the width stops being free.
5996
+ */
5997
+ speed: z.number().min(1).max(8).default(4)
5998
+ }), EventMediaProductionSchema, {
5999
+ kind: "mutation",
6000
+ auth: "admin"
6001
+ }),
6002
+ /**
6003
+ * Redeem a {@link EventMediaArtifactSchema} handle for its bytes.
6004
+ *
6005
+ * Separate from the production on purpose (D18): the producing runner keeps
6006
+ * the artifact for a few minutes, and a consumer that only wanted the
6007
+ * coverage never moves a megabyte. `null` once it has expired — a handle is
6008
+ * short-lived by design and a caller that waited too long must see that
6009
+ * rather than a zero-length file.
6010
+ */
6011
+ fetchEventMedia: method(z.object({ handle: z.string() }), z.object({
6012
+ base64: z.string(),
6013
+ mime: z.string(),
6014
+ bytes: z.number().int()
6015
+ }).nullable(), {
6016
+ kind: "mutation",
6017
+ auth: "admin"
6018
+ }),
5874
6019
  listAllCameraStreams: method(z.void(), z.array(CameraStreamSchema).readonly()),
5875
6020
  listAllProfileSlots: method(z.void(), z.array(ProfileSlotSchema).readonly()),
5876
6021
  getBrokerStats: method(z.object({ brokerId: z.string() }), BrokerStatsSchema),
@@ -6442,6 +6587,28 @@ function kebabToCamel(s) {
6442
6587
  * instead of taking the hub with it (D6). Every method here is admin-only, and
6443
6588
  * must stay so.
6444
6589
  */
6590
+ /**
6591
+ * Prefix for a block's generated addon id AND its runner id — one addon, one
6592
+ * process, so the two are the same string (D2).
6593
+ *
6594
+ * It lives on the CONTRACT rather than beside the code that materialises the
6595
+ * package, because it is not an implementation detail: the operator surface
6596
+ * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
6597
+ * with no cap and no store of its own. Two copies of this string is one copy
6598
+ * too many — a drift would show as a logs pane that is simply always empty.
6599
+ */
6600
+ var CORE_BLOCK_ADDON_PREFIX = "core-block-";
6601
+ /** The addon/runner id a block's process runs under. */
6602
+ function coreBlockAddonId(blockId) {
6603
+ return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
6604
+ }
6605
+ /** The block id behind a generated addon/runner id, or null when the id belongs
6606
+ * to something else. The inverse of {@link coreBlockAddonId}. */
6607
+ function coreBlockIdFromAddonId(addonId) {
6608
+ if (!addonId.startsWith("core-block-")) return null;
6609
+ const rest = addonId.slice(11);
6610
+ return rest.length > 0 ? rest : null;
6611
+ }
6445
6612
  /** Where a block runs. The operator chooses — a block driving a device on an
6446
6613
  * agent is the reason placement is not fixed to the hub. */
6447
6614
  var CoreBlockPlacementSchema = z.union([z.literal("hub"), z.string().min(1)]);
@@ -6530,6 +6697,23 @@ var coreBlocksCapability = {
6530
6697
  auth: "admin"
6531
6698
  }),
6532
6699
  /**
6700
+ * Stop this block's runner and let the supervisor bring it back.
6701
+ *
6702
+ * **It stores nothing.** There is no "restart requested" flag and no second
6703
+ * lifecycle field: the supervisor already stops and respawns a runner when
6704
+ * the code hash changes, and this exposes that same path deliberately
6705
+ * (D62 — a switch writes the authority that already owned the function).
6706
+ * The next reconcile pass remains the authority, so a restart that lost a
6707
+ * race is corrected within one pass rather than leaving a stale flag behind.
6708
+ *
6709
+ * Refused for a DISABLED block: nothing would come back, and a button that
6710
+ * silently does nothing is worse than one that says why.
6711
+ */
6712
+ restart: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema }), {
6713
+ kind: "mutation",
6714
+ auth: "admin"
6715
+ }),
6716
+ /**
6533
6717
  * Type-check without saving — what the editor calls as the author types, so
6534
6718
  * the compiler's verdict is the same one the server will reach.
6535
6719
  */
@@ -7570,895 +7754,159 @@ var deviceExportCapability = {
7570
7754
  unexposeDevice: method(UnexposeInputSchema, z.void(), { kind: "mutation" })
7571
7755
  }
7572
7756
  };
7757
+ var ProviderStatusSchema = z.object({
7758
+ connected: z.boolean(),
7759
+ deviceCount: z.number(),
7760
+ error: z.string().optional()
7761
+ });
7762
+ z.object({
7763
+ externalId: z.string(),
7764
+ name: z.string(),
7765
+ type: z.string(),
7766
+ metadata: z.record(z.string(), z.unknown()).optional()
7767
+ });
7573
7768
  /**
7574
- * Resource-bound constants for the safe expression engine.
7575
- *
7576
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
7577
- * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
7578
- * O(nodeCount) by construction. These caps merely put a hard ceiling on the
7579
- * work a single author-supplied expression can request, so a hostile or
7580
- * accidental pathological string can never spend unbounded CPU/memory.
7769
+ * Candidate handed back from discovery and accepted by
7770
+ * `adoptDiscoveredDevice`. Shape mirrors the in-process
7771
+ * `DiscoveredDevice` interface used by `DeviceDiscovery`.
7581
7772
  */
7582
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
7583
- * rejected without allocation. */
7584
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
7585
- /** A legal binding / identifier name. */
7586
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7587
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
7588
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
7589
- var RESERVED_BINDING_NAMES = new Set([
7590
- "now",
7591
- "true",
7592
- "false",
7593
- "null"
7594
- ]);
7773
+ var DiscoveryCandidateSchema = z.object({
7774
+ stableId: z.string(),
7775
+ type: z.enum(DeviceType),
7776
+ suggestedName: z.string(),
7777
+ prefilledConfig: z.record(z.string(), z.unknown()),
7778
+ /**
7779
+ * Optional upstream-system identity (HA entity_id, vendor MAC, …).
7780
+ * Discovery pre-populates this for systems that know the upstream
7781
+ * identity ahead of adoption. Rendering metadata (unit, precision)
7782
+ * flows live through the cap STATUS SLICE after adoption.
7783
+ */
7784
+ sourceInfo: SourceInfoSchema.optional()
7785
+ });
7595
7786
  /**
7596
- * Error types for the safe expression engine. Two distinct classes so callers
7597
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
7598
- * failure both are non-fatal to the host: read paths degrade to "skip link".
7787
+ * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
7788
+ * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
7789
+ * tRPC layer can pass it through without reshaping.
7599
7790
  */
7600
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
7601
- * the failure is anchored to a character (author-facing inline feedback). */
7602
- var ExpressionParseError = class extends Error {
7603
- position;
7604
- constructor(message, position) {
7605
- super(message);
7606
- this.name = "ExpressionParseError";
7607
- this.position = position;
7608
- }
7609
- };
7610
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
7611
- * result, unknown builtin, step-budget exceeded). */
7612
- var ExpressionEvalError = class extends Error {
7613
- constructor(message) {
7614
- super(message);
7615
- this.name = "ExpressionEvalError";
7791
+ var DeviceSummarySchema = z.object({
7792
+ id: z.number(),
7793
+ stableId: z.string(),
7794
+ addonId: z.string(),
7795
+ type: z.string(),
7796
+ name: z.string(),
7797
+ parentDeviceId: z.number().nullable(),
7798
+ online: z.boolean(),
7799
+ features: z.array(z.string()),
7800
+ config: z.record(z.string(), z.unknown()),
7801
+ /** Optional upstream-system identity (dispatch key + system tag).
7802
+ * See `SourceInfo`. Present when the device has a non-synthetic
7803
+ * source identifier (HA entities, vendor MAC, …); omitted when the
7804
+ * synthetic backfill is in effect. */
7805
+ sourceInfo: SourceInfoSchema.optional()
7806
+ });
7807
+ /**
7808
+ * Result of a live field test (e.g. probing an RTSP URL during device
7809
+ * creation). Matches the UI-side `FieldProbeResult` in
7810
+ * `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
7811
+ * returned `labels` as chips next to the input.
7812
+ */
7813
+ var FieldProbeResultSchema = z.object({
7814
+ status: z.enum(["ok", "error"]),
7815
+ labels: z.array(z.string()).optional(),
7816
+ error: z.string().optional()
7817
+ });
7818
+ /**
7819
+ * The output of `getChildCreationSchema` is a UI schema tree. We store
7820
+ * it as `unknown` at the capability layer — the router just passes it
7821
+ * through and the admin UI renders it via `FormBuilder`. The actual
7822
+ * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
7823
+ * but we deliberately avoid a Zod mirror because the union is large and
7824
+ * not meant for runtime validation at this seam.
7825
+ */
7826
+ var CreationSchemaOutputSchema = z.unknown();
7827
+ var deviceProviderCapability = {
7828
+ name: "device-provider",
7829
+ scope: "system",
7830
+ mode: "collection",
7831
+ methods: {
7832
+ start: method(z.void(), z.void(), { kind: "mutation" }),
7833
+ stop: method(z.void(), z.void(), { kind: "mutation" }),
7834
+ getStatus: method(z.void(), ProviderStatusSchema),
7835
+ getDevices: method(z.void(), z.array(z.object({
7836
+ id: z.string(),
7837
+ name: z.string(),
7838
+ type: z.string()
7839
+ }))),
7840
+ supportsDiscovery: method(z.object({}), z.boolean()),
7841
+ /**
7842
+ * Run a network scan. `params` carries optional provider-specific scan
7843
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
7844
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
7845
+ * (provider uses its local-network default).
7846
+ */
7847
+ discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
7848
+ kind: "mutation",
7849
+ auth: "admin"
7850
+ }),
7851
+ /**
7852
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
7853
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
7854
+ * `null` when the provider takes no extra scan params — the generic
7855
+ * aggregated scan never renders this; the per-integration scan does.
7856
+ */
7857
+ getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
7858
+ /**
7859
+ * The DeviceType this provider creates via manual add (Camera for
7860
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
7861
+ * provider does not support manual creation. Lets the Add-Device dialog
7862
+ * pick the right type instead of assuming Camera.
7863
+ */
7864
+ getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
7865
+ adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
7866
+ kind: "mutation",
7867
+ auth: "admin"
7868
+ }),
7869
+ supportsManualCreation: method(z.object({}), z.boolean()),
7870
+ /**
7871
+ * Fetch the creation form schema for a given DeviceType. Returns
7872
+ * `null` when the provider does not support manually creating
7873
+ * devices of that type. The output is a `ConfigUISchema` — the
7874
+ * router type-asserts it at the boundary.
7875
+ */
7876
+ getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
7877
+ createDevice: method(z.object({
7878
+ type: z.enum(DeviceType),
7879
+ config: z.record(z.string(), z.unknown())
7880
+ }), DeviceSummarySchema, {
7881
+ kind: "mutation",
7882
+ auth: "admin"
7883
+ }),
7884
+ /**
7885
+ * Test a single field in the creation form before the device has
7886
+ * been persisted. Typical use: probing an RTSP URL entered by the
7887
+ * user. Providers that don't support field probing return
7888
+ * `{ success: true, message: 'Field test not supported' }`.
7889
+ *
7890
+ * `formValues` is the live snapshot of every field in the form at
7891
+ * the moment the user clicked Test — useful for probes that depend
7892
+ * on multiple fields together (e.g. Reolink autodetect needs host
7893
+ * + credentials + UID + transport mode in a single call). Optional
7894
+ * for backwards compatibility; providers free to ignore it.
7895
+ */
7896
+ testCreationField: method(z.object({
7897
+ type: z.enum(DeviceType),
7898
+ key: z.string(),
7899
+ value: z.unknown(),
7900
+ formValues: z.record(z.string(), z.unknown()).optional()
7901
+ }), FieldProbeResultSchema, {
7902
+ kind: "mutation",
7903
+ auth: "admin"
7904
+ })
7616
7905
  }
7617
7906
  };
7618
7907
  /**
7619
- * Frozen, null-prototype builtin function table for the expression engine
7620
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
7621
- * parser rejects any callee not in it, and the evaluator gates each call on an
7622
- * own-property check against it.
7623
- *
7624
- * Because the object has a NULL prototype AND is `Object.freeze`d:
7625
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
7626
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
7627
- * (there is no `Object.prototype` in the chain), so those names are not
7628
- * callable — they are simply "unknown function" at parse time.
7629
- *
7630
- * Every numeric argument is validated as a finite number and every numeric
7631
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
7632
- * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
7633
- * closed rather than emitting a garbage value.
7634
- */
7635
- function asFiniteNumber(value, name, index) {
7636
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
7637
- return value;
7638
- }
7639
- function asString$1(value, name, index) {
7640
- if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
7641
- return value;
7642
- }
7643
- function finiteResult(value, name) {
7644
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
7645
- return value;
7646
- }
7647
- function allFiniteNumbers(args, name) {
7648
- return args.map((a, idx) => asFiniteNumber(a, name, idx));
7649
- }
7650
- var INF = Number.POSITIVE_INFINITY;
7651
- var table = {
7652
- min: {
7653
- minArgs: 1,
7654
- maxArgs: INF,
7655
- apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
7656
- },
7657
- max: {
7658
- minArgs: 1,
7659
- maxArgs: INF,
7660
- apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
7661
- },
7662
- abs: {
7663
- minArgs: 1,
7664
- maxArgs: 1,
7665
- apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
7666
- },
7667
- floor: {
7668
- minArgs: 1,
7669
- maxArgs: 1,
7670
- apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
7671
- },
7672
- ceil: {
7673
- minArgs: 1,
7674
- maxArgs: 1,
7675
- apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
7676
- },
7677
- sqrt: {
7678
- minArgs: 1,
7679
- maxArgs: 1,
7680
- apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
7681
- },
7682
- round: {
7683
- minArgs: 1,
7684
- maxArgs: 2,
7685
- apply: (args) => {
7686
- const x = asFiniteNumber(args[0], "round", 0);
7687
- const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
7688
- if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
7689
- const factor = 10 ** digits;
7690
- return finiteResult(Math.round(x * factor) / factor, "round");
7691
- }
7692
- },
7693
- pow: {
7694
- minArgs: 2,
7695
- maxArgs: 2,
7696
- apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
7697
- },
7698
- clamp: {
7699
- minArgs: 3,
7700
- maxArgs: 3,
7701
- apply: (args) => {
7702
- const x = asFiniteNumber(args[0], "clamp", 0);
7703
- const lo = asFiniteNumber(args[1], "clamp", 1);
7704
- const hi = asFiniteNumber(args[2], "clamp", 2);
7705
- if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
7706
- return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
7707
- }
7708
- },
7709
- avg: {
7710
- minArgs: 1,
7711
- maxArgs: INF,
7712
- apply: (args) => {
7713
- const nums = allFiniteNumbers(args, "avg");
7714
- return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
7715
- }
7716
- },
7717
- sum: {
7718
- minArgs: 1,
7719
- maxArgs: INF,
7720
- apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
7721
- },
7722
- coalesce: {
7723
- minArgs: 1,
7724
- maxArgs: INF,
7725
- apply: (args) => {
7726
- for (const a of args) if (a !== null) return a;
7727
- return null;
7728
- }
7729
- },
7730
- age: {
7731
- minArgs: 2,
7732
- maxArgs: 2,
7733
- apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
7734
- },
7735
- convert: {
7736
- minArgs: 3,
7737
- maxArgs: 3,
7738
- apply: (args, hooks) => {
7739
- const x = asFiniteNumber(args[0], "convert", 0);
7740
- const from = asString$1(args[1], "convert", 1).trim();
7741
- const to = asString$1(args[2], "convert", 2).trim();
7742
- if (hooks.convert) {
7743
- const out = hooks.convert(x, from, to);
7744
- if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
7745
- return finiteResult(out, "convert");
7746
- }
7747
- if (from === to) return x;
7748
- throw new ExpressionEvalError("convert: unit conversion table not installed");
7749
- }
7750
- }
7751
- };
7752
- /** Frozen, null-prototype builtin table. */
7753
- var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
7754
- /** The set of valid builtin names — used by the parser to reject unknown
7755
- * callees at parse time (immediate author feedback). */
7756
- var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
7757
- /**
7758
- * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
7759
- * zero-dependency. The grammar is deliberately boring: decimal numbers,
7760
- * single/double-quoted strings with a tiny escape set, identifiers, the three
7761
- * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
7762
- * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
7763
- * is a parse error with a source position, so member access / assignment /
7764
- * template literals are lexically impossible.
7765
- */
7766
- var KEYWORDS = new Set([
7767
- "true",
7768
- "false",
7769
- "null"
7770
- ]);
7771
- function isDigit(ch) {
7772
- return ch >= "0" && ch <= "9";
7773
- }
7774
- function isIdentStart(ch) {
7775
- return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
7776
- }
7777
- function isIdentPart(ch) {
7778
- return isIdentStart(ch) || isDigit(ch);
7779
- }
7780
- function isWhitespace(ch) {
7781
- return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
7782
- }
7783
- /** Tokenize `source` into a flat token list ending with a single `eof` token.
7784
- * Throws `ExpressionParseError` on any illegal character or unterminated
7785
- * string. */
7786
- function tokenize(source) {
7787
- if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
7788
- const tokens = [];
7789
- let i = 0;
7790
- const n = source.length;
7791
- while (i < n) {
7792
- const ch = source[i];
7793
- if (isWhitespace(ch)) {
7794
- i += 1;
7795
- continue;
7796
- }
7797
- if (isDigit(ch)) {
7798
- const start = i;
7799
- while (i < n && isDigit(source[i])) i += 1;
7800
- if (i < n && source[i] === ".") {
7801
- if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
7802
- i += 1;
7803
- while (i < n && isDigit(source[i])) i += 1;
7804
- }
7805
- const text = source.slice(start, i);
7806
- const value = Number(text);
7807
- if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
7808
- tokens.push({
7809
- type: "number",
7810
- value,
7811
- pos: start
7812
- });
7813
- continue;
7814
- }
7815
- if (ch === "'" || ch === "\"") {
7816
- const quote = ch;
7817
- const start = i;
7818
- i += 1;
7819
- let out = "";
7820
- let closed = false;
7821
- while (i < n) {
7822
- const c = source[i];
7823
- if (c === "\\") {
7824
- const next = i + 1 < n ? source[i + 1] : "";
7825
- if (next === "\\" || next === "'" || next === "\"") {
7826
- out += next;
7827
- i += 2;
7828
- continue;
7829
- }
7830
- throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
7831
- }
7832
- if (c === quote) {
7833
- closed = true;
7834
- i += 1;
7835
- break;
7836
- }
7837
- out += c;
7838
- i += 1;
7839
- }
7840
- if (!closed) throw new ExpressionParseError("unterminated string literal", start);
7841
- tokens.push({
7842
- type: "string",
7843
- value: out,
7844
- pos: start
7845
- });
7846
- continue;
7847
- }
7848
- if (isIdentStart(ch)) {
7849
- const start = i;
7850
- while (i < n && isIdentPart(source[i])) i += 1;
7851
- const text = source.slice(start, i);
7852
- if (KEYWORDS.has(text)) tokens.push({
7853
- type: "keyword",
7854
- keyword: keywordOf(text),
7855
- pos: start
7856
- });
7857
- else tokens.push({
7858
- type: "identifier",
7859
- name: text,
7860
- pos: start
7861
- });
7862
- continue;
7863
- }
7864
- const two = i + 1 < n ? source.slice(i, i + 2) : "";
7865
- if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
7866
- tokens.push({
7867
- type: "punct",
7868
- punct: two,
7869
- pos: i
7870
- });
7871
- i += 2;
7872
- continue;
7873
- }
7874
- if (isSinglePunct(ch)) {
7875
- tokens.push({
7876
- type: "punct",
7877
- punct: ch,
7878
- pos: i
7879
- });
7880
- i += 1;
7881
- continue;
7882
- }
7883
- throw new ExpressionParseError(`unexpected character '${ch}'`, i);
7884
- }
7885
- tokens.push({
7886
- type: "eof",
7887
- pos: n
7888
- });
7889
- return tokens;
7890
- }
7891
- function keywordOf(text) {
7892
- if (text === "true") return "true";
7893
- if (text === "false") return "false";
7894
- return "null";
7895
- }
7896
- function isSinglePunct(ch) {
7897
- return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
7898
- }
7899
- /**
7900
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
7901
- *
7902
- * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
7903
- * → relational → additive → multiplicative → unary `! -` → call / primary.
7904
- * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
7905
- * string validated against the builtin table at parse time, so an unknown
7906
- * function is rejected immediately (author feedback) and a persisted expression
7907
- * that references a since-removed builtin degrades at read.
7908
- *
7909
- * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
7910
- * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
7911
- */
7912
- /** Binary/logical operator precedence (higher binds tighter). */
7913
- var BINARY_PRECEDENCE = {
7914
- "||": 1,
7915
- "&&": 2,
7916
- "==": 3,
7917
- "!=": 3,
7918
- "<": 4,
7919
- "<=": 4,
7920
- ">": 4,
7921
- ">=": 4,
7922
- "+": 5,
7923
- "-": 5,
7924
- "*": 6,
7925
- "/": 6,
7926
- "%": 6
7927
- };
7928
- function isLogicalOp(op) {
7929
- return op === "&&" || op === "||";
7930
- }
7931
- function isBinaryOp(op) {
7932
- return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
7933
- }
7934
- var Parser = class {
7935
- tokens;
7936
- pos = 0;
7937
- nodeCount = 0;
7938
- identifiers = /* @__PURE__ */ new Set();
7939
- callees = /* @__PURE__ */ new Set();
7940
- constructor(tokens) {
7941
- this.tokens = tokens;
7942
- }
7943
- parse() {
7944
- const ast = this.parseTernary();
7945
- const tok = this.peek();
7946
- if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
7947
- return {
7948
- ast,
7949
- identifiers: this.identifiers,
7950
- callees: this.callees,
7951
- nodeCount: this.nodeCount
7952
- };
7953
- }
7954
- peek() {
7955
- return this.tokens[this.pos];
7956
- }
7957
- next() {
7958
- return this.tokens[this.pos++];
7959
- }
7960
- /** Consume a punctuator token, erroring if the next token isn't it. */
7961
- expectPunct(punct) {
7962
- const tok = this.peek();
7963
- if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
7964
- this.pos += 1;
7965
- }
7966
- matchPunct(punct) {
7967
- const tok = this.peek();
7968
- if (tok.type === "punct" && tok.punct === punct) {
7969
- this.pos += 1;
7970
- return true;
7971
- }
7972
- return false;
7973
- }
7974
- countNode() {
7975
- this.nodeCount += 1;
7976
- if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
7977
- }
7978
- parseTernary() {
7979
- const test = this.parseBinary(1);
7980
- if (this.matchPunct("?")) {
7981
- const consequent = this.parseTernary();
7982
- this.expectPunct(":");
7983
- const alternate = this.parseTernary();
7984
- this.countNode();
7985
- return {
7986
- kind: "conditional",
7987
- test,
7988
- consequent,
7989
- alternate
7990
- };
7991
- }
7992
- return test;
7993
- }
7994
- parseBinary(minPrec) {
7995
- let left = this.parseUnary();
7996
- for (;;) {
7997
- const tok = this.peek();
7998
- if (tok.type !== "punct") break;
7999
- const prec = BINARY_PRECEDENCE[tok.punct];
8000
- if (prec === void 0 || prec < minPrec) break;
8001
- const op = tok.punct;
8002
- this.pos += 1;
8003
- const right = this.parseBinary(prec + 1);
8004
- this.countNode();
8005
- if (isLogicalOp(op)) left = {
8006
- kind: "logical",
8007
- op,
8008
- left,
8009
- right
8010
- };
8011
- else if (isBinaryOp(op)) left = {
8012
- kind: "binary",
8013
- op,
8014
- left,
8015
- right
8016
- };
8017
- else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
8018
- }
8019
- return left;
8020
- }
8021
- parseUnary() {
8022
- const tok = this.peek();
8023
- if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
8024
- const op = tok.punct;
8025
- this.pos += 1;
8026
- const operand = this.parseUnary();
8027
- this.countNode();
8028
- return {
8029
- kind: "unary",
8030
- op,
8031
- operand
8032
- };
8033
- }
8034
- return this.parsePrimary();
8035
- }
8036
- parsePrimary() {
8037
- const tok = this.next();
8038
- switch (tok.type) {
8039
- case "number":
8040
- this.countNode();
8041
- return {
8042
- kind: "literal",
8043
- value: tok.value
8044
- };
8045
- case "string":
8046
- this.countNode();
8047
- return {
8048
- kind: "literal",
8049
- value: tok.value
8050
- };
8051
- case "keyword":
8052
- this.countNode();
8053
- return {
8054
- kind: "literal",
8055
- value: tok.keyword === "null" ? null : tok.keyword === "true"
8056
- };
8057
- case "identifier": {
8058
- const nextTok = this.peek();
8059
- if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
8060
- this.identifiers.add(tok.name);
8061
- this.countNode();
8062
- return {
8063
- kind: "identifier",
8064
- name: tok.name
8065
- };
8066
- }
8067
- case "punct":
8068
- if (tok.punct === "(") {
8069
- const inner = this.parseTernary();
8070
- this.expectPunct(")");
8071
- return inner;
8072
- }
8073
- throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
8074
- case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
8075
- }
8076
- }
8077
- parseCall(callee, pos) {
8078
- if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
8079
- this.expectPunct("(");
8080
- const args = [];
8081
- if (!this.matchPunct(")")) for (;;) {
8082
- args.push(this.parseTernary());
8083
- if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
8084
- if (this.matchPunct(",")) continue;
8085
- this.expectPunct(")");
8086
- break;
8087
- }
8088
- this.callees.add(callee);
8089
- this.countNode();
8090
- return {
8091
- kind: "call",
8092
- callee,
8093
- args
8094
- };
8095
- }
8096
- };
8097
- /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
8098
- * `ExpressionParseError` on any lexical or grammatical failure. */
8099
- function parseExpression(source) {
8100
- return new Parser(tokenize(source)).parse();
8101
- }
8102
- /**
8103
- * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
8104
- * by expr"). The cache stores BOTH successes and failures (negative caching),
8105
- * so a corrupt persisted string costs exactly one tokenize+parse total — not
8106
- * one per read on a hot resolve path.
8107
- *
8108
- * The cache is a module-level singleton: entries are pure, content-addressed
8109
- * ASTs keyed by the raw source string, so sharing one instance across all
8110
- * callers is safe and maximises hit rate.
8111
- */
8112
- var cache = /* @__PURE__ */ new Map();
8113
- function getCached(source) {
8114
- const hit = cache.get(source);
8115
- if (hit !== void 0) {
8116
- cache.delete(source);
8117
- cache.set(source, hit);
8118
- return hit;
8119
- }
8120
- let result;
8121
- try {
8122
- result = {
8123
- ok: true,
8124
- parsed: parseExpression(source)
8125
- };
8126
- } catch (err) {
8127
- result = {
8128
- ok: false,
8129
- error: err instanceof ExpressionParseError ? err.message : String(err)
8130
- };
8131
- }
8132
- cache.set(source, result);
8133
- if (cache.size > 256) {
8134
- const oldest = cache.keys().next().value;
8135
- if (oldest !== void 0) cache.delete(oldest);
8136
- }
8137
- return result;
8138
- }
8139
- /** Compile `source`, returning a discriminated result instead of throwing.
8140
- * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
8141
- function compileExpressionSafe(source) {
8142
- return getCached(source);
8143
- }
8144
- /**
8145
- * Tree-walking evaluator for the safe expression mini-language.
8146
- *
8147
- * SECURITY (spec §4 rule 2/5):
8148
- * - The scope is an `Object.create(null)` copy of ONLY the caller's own
8149
- * enumerable binding entries, so `name in scope` is a pure own-key check and
8150
- * `constructor` / `__proto__` / `toString` are plain unknown identifiers.
8151
- * - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
8152
- * directly — the only external calls are into the frozen builtin table.
8153
- * - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
8154
- * construction; the step counter is defense-in-depth for a crafted max-size
8155
- * AST. Nothing blocks: there are no timers, awaits or unbounded loops.
8156
- */
8157
- var EMPTY_HOOKS = Object.freeze({});
8158
- /** Build a null-prototype scope from own-enumerable binding entries. Inherited
8159
- * keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
8160
- * so nothing smuggles in via the prototype chain. */
8161
- function createExpressionScope(bindings) {
8162
- const scope = Object.create(null);
8163
- for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
8164
- return scope;
8165
- }
8166
- function isFiniteNumber(value) {
8167
- return typeof value === "number" && Number.isFinite(value);
8168
- }
8169
- /** JS truthiness of a primitive value. */
8170
- function truthy(value) {
8171
- return Boolean(value);
8172
- }
8173
- function requireFinite(value, context) {
8174
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
8175
- return value;
8176
- }
8177
- function step(ctx) {
8178
- ctx.steps += 1;
8179
- if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
8180
- }
8181
- function evalNode(node, ctx) {
8182
- step(ctx);
8183
- switch (node.kind) {
8184
- case "literal": return node.value;
8185
- case "identifier":
8186
- if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
8187
- return ctx.scope[node.name];
8188
- case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
8189
- case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
8190
- case "logical": {
8191
- const left = evalNode(node.left, ctx);
8192
- if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
8193
- return truthy(left) ? left : evalNode(node.right, ctx);
8194
- }
8195
- case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
8196
- case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
8197
- }
8198
- }
8199
- function evalUnary(op, operand) {
8200
- if (op === "!") return !truthy(operand);
8201
- if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
8202
- return requireFinite(-operand, "unary \"-\"");
8203
- }
8204
- function evalBinary(op, left, right) {
8205
- switch (op) {
8206
- case "==": return left === right;
8207
- case "!=": return left !== right;
8208
- case "+":
8209
- if (typeof left === "string" && typeof right === "string") return left + right;
8210
- if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
8211
- throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
8212
- case "-":
8213
- case "*":
8214
- case "/":
8215
- case "%":
8216
- if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
8217
- return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
8218
- case "<":
8219
- case "<=":
8220
- case ">":
8221
- case ">=":
8222
- if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
8223
- if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
8224
- throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
8225
- }
8226
- }
8227
- function evalCall(callee, args, hooks) {
8228
- if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
8229
- const builtin = EXPRESSION_BUILTINS[callee];
8230
- if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
8231
- return builtin.apply(args, hooks);
8232
- }
8233
- /** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
8234
- * runtime failure (unknown identifier, type mismatch, non-finite result,
8235
- * step-budget exhaustion). */
8236
- function evaluateAst(node, scope, opts) {
8237
- return evalNode(node, {
8238
- scope,
8239
- hooks: opts?.hooks ?? EMPTY_HOOKS,
8240
- maxSteps: opts?.maxSteps ?? 4096,
8241
- steps: 0
8242
- });
8243
- }
8244
- /**
8245
- * Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
8246
- * Non-primitive values (objects, arrays, `undefined`, functions, bigint,
8247
- * symbol) and non-finite numbers become `undefined` so the caller can apply
8248
- * its binding-miss policy (→ `null`). `null` itself is a valid value.
8249
- */
8250
- function toExpressionValue(raw) {
8251
- if (raw === null) return null;
8252
- if (typeof raw === "string") return raw;
8253
- if (typeof raw === "boolean") return raw;
8254
- if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
8255
- }
8256
- /**
8257
- * Author-time validation. Returns `null` when the source is valid, else a
8258
- * human-readable error message. Checks: the expression compiles; binding count
8259
- * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
8260
- * is not reserved (`now`/keywords) and does not shadow a builtin; and every
8261
- * FREE identifier of the AST is covered by a binding or the injected `now`.
8262
- */
8263
- function validateExpressionSource(src) {
8264
- const names = Object.keys(src.bindings);
8265
- if (names.length > 32) return `too many bindings (${names.length} > 32)`;
8266
- for (const name of names) {
8267
- if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
8268
- if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
8269
- if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
8270
- }
8271
- const compiled = compileExpressionSafe(src.expr);
8272
- if (!compiled.ok) return compiled.error;
8273
- const bound = new Set(names);
8274
- for (const id of compiled.parsed.identifiers) {
8275
- if (id === "now") continue;
8276
- if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
8277
- }
8278
- return null;
8279
- }
8280
- /**
8281
- * Shared read-path evaluation for BOTH resolver channels. Builds a null-proto
8282
- * scope from `bindingValues` plus the injected `now` (supplied by the caller
8283
- * for determinism/testability), compiles via the LRU, and evaluates. Any
8284
- * failure (parse or eval) returns `{ ok: false }` — the caller treats that as
8285
- * "skip this link".
8286
- */
8287
- function evaluateLinkExpression(expr, bindingValues, now, opts) {
8288
- const compiled = compileExpressionSafe(expr);
8289
- if (!compiled.ok) return {
8290
- ok: false,
8291
- error: compiled.error
8292
- };
8293
- const scope = createExpressionScope({
8294
- ...bindingValues,
8295
- ["now"]: now
8296
- });
8297
- try {
8298
- return {
8299
- ok: true,
8300
- value: evaluateAst(compiled.parsed.ast, scope, opts)
8301
- };
8302
- } catch (err) {
8303
- return {
8304
- ok: false,
8305
- error: err instanceof ExpressionEvalError ? err.message : String(err)
8306
- };
8307
- }
8308
- }
8309
- var ProviderStatusSchema = z.object({
8310
- connected: z.boolean(),
8311
- deviceCount: z.number(),
8312
- error: z.string().optional()
8313
- });
8314
- z.object({
8315
- externalId: z.string(),
8316
- name: z.string(),
8317
- type: z.string(),
8318
- metadata: z.record(z.string(), z.unknown()).optional()
8319
- });
8320
- /**
8321
- * Candidate handed back from discovery and accepted by
8322
- * `adoptDiscoveredDevice`. Shape mirrors the in-process
8323
- * `DiscoveredDevice` interface used by `DeviceDiscovery`.
8324
- */
8325
- var DiscoveryCandidateSchema = z.object({
8326
- stableId: z.string(),
8327
- type: z.enum(DeviceType),
8328
- suggestedName: z.string(),
8329
- prefilledConfig: z.record(z.string(), z.unknown()),
8330
- /**
8331
- * Optional upstream-system identity (HA entity_id, vendor MAC, …).
8332
- * Discovery pre-populates this for systems that know the upstream
8333
- * identity ahead of adoption. Rendering metadata (unit, precision)
8334
- * flows live through the cap STATUS SLICE after adoption.
8335
- */
8336
- sourceInfo: SourceInfoSchema.optional()
8337
- });
8338
- /**
8339
- * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
8340
- * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
8341
- * tRPC layer can pass it through without reshaping.
8342
- */
8343
- var DeviceSummarySchema = z.object({
8344
- id: z.number(),
8345
- stableId: z.string(),
8346
- addonId: z.string(),
8347
- type: z.string(),
8348
- name: z.string(),
8349
- parentDeviceId: z.number().nullable(),
8350
- online: z.boolean(),
8351
- features: z.array(z.string()),
8352
- config: z.record(z.string(), z.unknown()),
8353
- /** Optional upstream-system identity (dispatch key + system tag).
8354
- * See `SourceInfo`. Present when the device has a non-synthetic
8355
- * source identifier (HA entities, vendor MAC, …); omitted when the
8356
- * synthetic backfill is in effect. */
8357
- sourceInfo: SourceInfoSchema.optional()
8358
- });
8359
- /**
8360
- * Result of a live field test (e.g. probing an RTSP URL during device
8361
- * creation). Matches the UI-side `FieldProbeResult` in
8362
- * `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
8363
- * returned `labels` as chips next to the input.
8364
- */
8365
- var FieldProbeResultSchema = z.object({
8366
- status: z.enum(["ok", "error"]),
8367
- labels: z.array(z.string()).optional(),
8368
- error: z.string().optional()
8369
- });
8370
- /**
8371
- * The output of `getChildCreationSchema` is a UI schema tree. We store
8372
- * it as `unknown` at the capability layer — the router just passes it
8373
- * through and the admin UI renders it via `FormBuilder`. The actual
8374
- * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
8375
- * but we deliberately avoid a Zod mirror because the union is large and
8376
- * not meant for runtime validation at this seam.
8377
- */
8378
- var CreationSchemaOutputSchema = z.unknown();
8379
- var deviceProviderCapability = {
8380
- name: "device-provider",
8381
- scope: "system",
8382
- mode: "collection",
8383
- methods: {
8384
- start: method(z.void(), z.void(), { kind: "mutation" }),
8385
- stop: method(z.void(), z.void(), { kind: "mutation" }),
8386
- getStatus: method(z.void(), ProviderStatusSchema),
8387
- getDevices: method(z.void(), z.array(z.object({
8388
- id: z.string(),
8389
- name: z.string(),
8390
- type: z.string()
8391
- }))),
8392
- supportsDiscovery: method(z.object({}), z.boolean()),
8393
- /**
8394
- * Run a network scan. `params` carries optional provider-specific scan
8395
- * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
8396
- * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
8397
- * (provider uses its local-network default).
8398
- */
8399
- discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
8400
- kind: "mutation",
8401
- auth: "admin"
8402
- }),
8403
- /**
8404
- * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
8405
- * provider accepts (e.g. Gree's broadcast address for a different subnet).
8406
- * `null` when the provider takes no extra scan params — the generic
8407
- * aggregated scan never renders this; the per-integration scan does.
8408
- */
8409
- getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
8410
- /**
8411
- * The DeviceType this provider creates via manual add (Camera for
8412
- * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
8413
- * provider does not support manual creation. Lets the Add-Device dialog
8414
- * pick the right type instead of assuming Camera.
8415
- */
8416
- getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
8417
- adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
8418
- kind: "mutation",
8419
- auth: "admin"
8420
- }),
8421
- supportsManualCreation: method(z.object({}), z.boolean()),
8422
- /**
8423
- * Fetch the creation form schema for a given DeviceType. Returns
8424
- * `null` when the provider does not support manually creating
8425
- * devices of that type. The output is a `ConfigUISchema` — the
8426
- * router type-asserts it at the boundary.
8427
- */
8428
- getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
8429
- createDevice: method(z.object({
8430
- type: z.enum(DeviceType),
8431
- config: z.record(z.string(), z.unknown())
8432
- }), DeviceSummarySchema, {
8433
- kind: "mutation",
8434
- auth: "admin"
8435
- }),
8436
- /**
8437
- * Test a single field in the creation form before the device has
8438
- * been persisted. Typical use: probing an RTSP URL entered by the
8439
- * user. Providers that don't support field probing return
8440
- * `{ success: true, message: 'Field test not supported' }`.
8441
- *
8442
- * `formValues` is the live snapshot of every field in the form at
8443
- * the moment the user clicked Test — useful for probes that depend
8444
- * on multiple fields together (e.g. Reolink autodetect needs host
8445
- * + credentials + UID + transport mode in a single call). Optional
8446
- * for backwards compatibility; providers free to ignore it.
8447
- */
8448
- testCreationField: method(z.object({
8449
- type: z.enum(DeviceType),
8450
- key: z.string(),
8451
- value: z.unknown(),
8452
- formValues: z.record(z.string(), z.unknown()).optional()
8453
- }), FieldProbeResultSchema, {
8454
- kind: "mutation",
8455
- auth: "admin"
8456
- })
8457
- }
8458
- };
8459
- /**
8460
- * Device Manager capability — hub-side singleton that unifies device persistence,
8461
- * live registry access, and all management operations into a single tRPC surface.
7908
+ * Device Manager capability hub-side singleton that unifies device persistence,
7909
+ * live registry access, and all management operations into a single tRPC surface.
8462
7910
  *
8463
7911
  * Replaces:
8464
7912
  * - `device-persistence` capability (persistence methods absorbed here)
@@ -8485,92 +7933,6 @@ var ChildLayoutEntrySchema = z.object({
8485
7933
  order: z.number().optional(),
8486
7934
  collapsed: z.boolean().optional()
8487
7935
  });
8488
- /** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
8489
- * `device-management.ts`. Source is a union: a FIELD source copies a sibling
8490
- * accessory's status field (`kind` optional/absent for wire compat); a
8491
- * LITERAL source carries a per-device constant (no sibling is read); a
8492
- * GLOBAL source (P2e) copies ANY device's status field, addressed by the
8493
- * source device's full re-sync-stable `stableId`. */
8494
- var DeviceLinkFieldSourceSchema = z.object({
8495
- kind: z.literal("field").optional(),
8496
- sourceKey: z.string(),
8497
- cap: z.string(),
8498
- fieldPath: z.string()
8499
- });
8500
- var DeviceLinkLiteralSourceSchema = z.object({
8501
- kind: z.literal("literal"),
8502
- value: z.union([
8503
- z.string(),
8504
- z.number(),
8505
- z.boolean(),
8506
- z.null()
8507
- ])
8508
- });
8509
- var DeviceLinkGlobalSourceSchema = z.object({
8510
- kind: z.literal("global"),
8511
- sourceStableId: z.string(),
8512
- cap: z.string(),
8513
- fieldPath: z.string()
8514
- });
8515
- /** Expression source (Stage X): compute the target field from N named bindings
8516
- * via the safe expression engine. Bindings are field | literal | global — never
8517
- * another expression (no nesting). The `superRefine` runs the SAME author-time
8518
- * validation as `validateExpressionSource` (compiles the expr, checks binding
8519
- * names + identifier coverage) so every wire boundary that parses a DeviceLink
8520
- * (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
8521
- * Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
8522
- var DeviceLinkExpressionSourceSchema = z.object({
8523
- kind: z.literal("expression"),
8524
- expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
8525
- bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), z.union([
8526
- DeviceLinkFieldSourceSchema,
8527
- DeviceLinkLiteralSourceSchema,
8528
- DeviceLinkGlobalSourceSchema
8529
- ]))
8530
- }).superRefine((src, ctx) => {
8531
- const err = validateExpressionSource(src);
8532
- if (err !== null) ctx.addIssue({
8533
- code: "custom",
8534
- message: err,
8535
- path: ["expr"]
8536
- });
8537
- });
8538
- var DeviceLinkSchema = z.object({
8539
- id: z.string(),
8540
- source: z.union([
8541
- DeviceLinkFieldSourceSchema,
8542
- DeviceLinkLiteralSourceSchema,
8543
- DeviceLinkGlobalSourceSchema,
8544
- DeviceLinkExpressionSourceSchema
8545
- ]),
8546
- target: z.object({
8547
- cap: z.string(),
8548
- fieldPath: z.string(),
8549
- itemKey: z.string().optional()
8550
- }),
8551
- transform: z.discriminatedUnion("kind", [
8552
- z.object({ kind: z.literal("identity") }),
8553
- z.object({
8554
- kind: z.literal("enum-map"),
8555
- mapping: z.record(z.string(), z.union([
8556
- z.string(),
8557
- z.number(),
8558
- z.boolean()
8559
- ])),
8560
- fallback: z.union([
8561
- z.string(),
8562
- z.number(),
8563
- z.boolean()
8564
- ]).optional()
8565
- }),
8566
- z.object({
8567
- kind: z.literal("linear"),
8568
- scale: z.number(),
8569
- offset: z.number(),
8570
- clamp: z.tuple([z.number(), z.number()]).readonly().optional()
8571
- })
8572
- ]).optional()
8573
- });
8574
7936
  /** Cap-wire shape of a per-cap display refinement — mirrors
8575
7937
  * `DeviceCapDisplayOverride` in `device-management.ts`. */
8576
7938
  var DeviceCapDisplayOverrideSchema = z.object({
@@ -8650,8 +8012,6 @@ var DeviceInfoSchema = z.object({
8650
8012
  * named accordion sections (with optional intra-section order). See
8651
8013
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
8652
8014
  childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
8653
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
8654
- deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
8655
8015
  /** Operator-authored per-device display override. See `DeviceMeta.display`. */
8656
8016
  display: DeviceDisplayOverrideSchema.optional()
8657
8017
  });
@@ -8660,7 +8020,7 @@ var ConfigEntrySchema = z.object({
8660
8020
  value: z.unknown(),
8661
8021
  description: z.string().optional()
8662
8022
  });
8663
- var DeviceLinkModeSchema = z.enum(["auto", "manual"]);
8023
+ var LinkedDevicesModeSchema = z.enum(["auto", "manual"]);
8664
8024
  /** One resolved linked device — the compact projection consumers need. */
8665
8025
  var LinkedDeviceSchema = z.object({
8666
8026
  deviceId: z.number(),
@@ -8723,8 +8083,6 @@ var DeviceMetaSchema = z.object({
8723
8083
  * accordion sections (with optional intra-section order). See
8724
8084
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
8725
8085
  childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
8726
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
8727
- deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
8728
8086
  /** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
8729
8087
  * Optional: only present for accessory children that carry a known role. */
8730
8088
  role: z.string().nullable().optional(),
@@ -8876,16 +8234,6 @@ var deviceManagerCapability = {
8876
8234
  kind: "mutation",
8877
8235
  auth: "admin"
8878
8236
  }),
8879
- /** Set (or replace) the cross-device field wirings on a device's meta row.
8880
- * Mirrors `setChildLayout`; persisted, projected, preserved across
8881
- * re-register/restore. Idempotent. */
8882
- setDeviceLinks: method(z.object({
8883
- deviceId: z.number(),
8884
- deviceLinks: z.array(DeviceLinkSchema).readonly()
8885
- }), z.void(), {
8886
- kind: "mutation",
8887
- auth: "admin"
8888
- }),
8889
8237
  /** Set (or clear) the per-device display override on the meta row. Mirrors
8890
8238
  * `setChildLayout` persistence; `null` clears the override entirely. The
8891
8239
  * override unit(s) are normalized (`normalizeUnit`) at write so the render
@@ -8909,15 +8257,15 @@ var deviceManagerCapability = {
8909
8257
  kind: "mutation",
8910
8258
  auth: "admin"
8911
8259
  }),
8912
- /** List the wireable status-schema fields per cap bound to a device.
8913
- * Powers the Wiring tab's field pickers. Caps without a status schema are
8914
- * omitted. Item-array caps (`status.itemArray`, e.g. consumables) also
8915
- * emit their per-item fields tagged `item: true` (a link targeting one
8916
- * must carry a `target.itemKey`) plus the cap-level `itemArray`
8917
- * descriptor. `includeSynthesizable: true` (TARGET pickers only) unions
8918
- * in unbound device-scoped caps that declare `status.empty` and match
8919
- * the device's type so the FIRST link to a synthesize-only cap
8920
- * (consumables on an HA vacuum) can be authored. */
8260
+ /** The per-cap catalog of readable status-schema fields for one device.
8261
+ * This is the SOURCE PICKER it outlived the Wiring tab that first
8262
+ * needed it (deleted 2026-08-08) and becomes the picker of a composition
8263
+ * recipe: every field the old tab could target, a composed device can
8264
+ * read. Caps without a status schema are omitted. Item-array caps
8265
+ * (`status.itemArray`, e.g. consumables) also emit their per-item fields
8266
+ * tagged `item: true` plus the cap-level `itemArray` descriptor.
8267
+ * `includeSynthesizable: true` unions in unbound device-scoped caps that
8268
+ * declare `status.empty` and match the device's type. */
8921
8269
  getWireableFields: method(z.object({
8922
8270
  deviceId: z.number(),
8923
8271
  includeSynthesizable: z.boolean().optional()
@@ -9059,7 +8407,7 @@ var deviceManagerCapability = {
9059
8407
  * (see the module docblock) — there are no bespoke link mutations.
9060
8408
  */
9061
8409
  getLinkedDevices: method(z.object({ deviceId: z.number() }), z.object({
9062
- mode: DeviceLinkModeSchema,
8410
+ mode: LinkedDevicesModeSchema,
9063
8411
  devices: z.array(LinkedDeviceSchema)
9064
8412
  })),
9065
8413
  /** Get stream sources for a camera device. */
@@ -9123,11 +8471,7 @@ var deviceManagerCapability = {
9123
8471
  deviceId: z.number(),
9124
8472
  entries: z.array(z.object({
9125
8473
  capName: z.string(),
9126
- kind: z.enum([
9127
- "native",
9128
- "wrapped",
9129
- "linked"
9130
- ]),
8474
+ kind: z.enum(["native", "wrapped"]),
9131
8475
  providerAddonId: z.string(),
9132
8476
  providerNodeId: z.string(),
9133
8477
  nativeAddonId: z.string()
@@ -9145,11 +8489,7 @@ var deviceManagerCapability = {
9145
8489
  deviceId: z.number(),
9146
8490
  entries: z.array(z.object({
9147
8491
  capName: z.string(),
9148
- kind: z.enum([
9149
- "native",
9150
- "wrapped",
9151
- "linked"
9152
- ]),
8492
+ kind: z.enum(["native", "wrapped"]),
9153
8493
  providerAddonId: z.string(),
9154
8494
  providerNodeId: z.string(),
9155
8495
  nativeAddonId: z.string()
@@ -10357,6 +9697,10 @@ var motionDetectionCapability = {
10357
9697
  mode: "singleton",
10358
9698
  kind: "wrapper",
10359
9699
  defaultActive: true,
9700
+ /** Frame-differencing motion analysis over a video stream — only a camera
9701
+ * produces the frames. Read by the `defaultActive` auto-bind in
9702
+ * `device-manager.getBindings` to decide which devices it may claim. */
9703
+ deviceTypes: [DeviceType.Camera],
10360
9704
  exposesDeviceSettings: true,
10361
9705
  methods: {
10362
9706
  analyze: method(z.object({
@@ -12750,6 +12094,18 @@ var OauthIntegrationDescriptorSchema = z.object({
12750
12094
  * redirect_uri that does not start with one of these. Required —
12751
12095
  * an empty list means the integration can never complete linking. */
12752
12096
  allowedRedirectPrefixes: z.array(z.string()).min(1),
12097
+ /** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
12098
+ * RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
12099
+ * `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
12100
+ * whose address the hub cannot know in advance (a Home Assistant at
12101
+ * `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
12102
+ * exactly; a public host never satisfies this branch, so it is not a
12103
+ * wildcard prefix by another name. */
12104
+ allowedPrivateHostPaths: z.array(z.string()).optional(),
12105
+ /** When true this is a PUBLIC client (source is published, no secret can be
12106
+ * protected) and PKCE is mandatory: `/authorize` refuses without an S256
12107
+ * `code_challenge`, `/token` refuses without the matching `code_verifier`. */
12108
+ requiresPkce: z.boolean().optional(),
12753
12109
  /** Optional public origin (no trailing slash) that this integration's
12754
12110
  * issued codes/tokens should carry as the `hubUrl` claim — typically the
12755
12111
  * operator-selected external-access endpoint resolved by the addon. When
@@ -12906,7 +12262,7 @@ var TrackEnvelopeSchema = z.object({
12906
12262
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
12907
12263
  * keeps every scalar the list surfaces actually render (ids, class(es),
12908
12264
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
12909
- * zonesVisited, bestEventId, envelope) and returns `positions` /
12265
+ * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
12910
12266
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
12911
12267
  * `getTrack`. Mirrors the event-store `projection` convention
12912
12268
  * (`getObjectEvents` et al.).
@@ -13143,6 +12499,24 @@ var TrackSchema = z.object({
13143
12499
  * Populated from the persisted envelope columns on historical reads;
13144
12500
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
13145
12501
  envelope: TrackEnvelopeSchema.optional(),
12502
+ /**
12503
+ * A face DETECTOR found a face on this track — nothing more. It says the
12504
+ * detail plane produced a `face` detail; it does NOT say the face was
12505
+ * embedded, matched, above `minFacePx`, or that the recognizer was even
12506
+ * enabled. Set once and never cleared.
12507
+ *
12508
+ * **This exists so "face present but not recognised" is expressible.** A
12509
+ * recognised identity lands in `subLabel` (attributed to the face chain via
12510
+ * `subLabelMeta.stepId`), so before this field a track with an unmatched face
12511
+ * and a track with no face at all were byte-identical on the wire and no
12512
+ * surface could tell them apart. The read is `hasFace === true && subLabel
12513
+ * === undefined`.
12514
+ *
12515
+ * **Absent ≠ false.** Every row written before the column existed omits it,
12516
+ * and so does every server that predates the field — a consumer must test
12517
+ * `=== true` and render nothing otherwise, never infer "no face".
12518
+ */
12519
+ hasFace: z.boolean().optional(),
13146
12520
  ...TrackFlagFields,
13147
12521
  ...TrackRetrainFields
13148
12522
  });
@@ -16892,6 +16266,10 @@ var SsoBridgeClaimsSchema = z.object({
16892
16266
  integrationId: z.string().optional(),
16893
16267
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
16894
16268
  jti: z.string().optional(),
16269
+ /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
16270
+ * client. Its PRESENCE is what makes the verifier mandatory at exchange,
16271
+ * so the requirement travels with the code and not with mutable config. */
16272
+ codeChallenge: z.string().optional(),
16895
16273
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
16896
16274
  * tokens so the verify path can check the session is not revoked. */
16897
16275
  sessionId: z.string().optional()
@@ -17648,6 +17026,10 @@ var videoclipsCapability = {
17648
17026
  mode: "singleton",
17649
17027
  kind: "wrapper",
17650
17028
  defaultActive: true,
17029
+ /** A clip is a window over a camera's footage — the cap is meaningless on a
17030
+ * sensor, a button or an event emitter, and the `defaultActive` auto-bind
17031
+ * reads this to decide which devices it may claim. */
17032
+ deviceTypes: [DeviceType.Camera],
17651
17033
  methods: {
17652
17034
  listClips: method(z.object({
17653
17035
  deviceId: z.number(),
@@ -25574,14 +24956,19 @@ var userManagementCapability = {
25574
24956
  username: z.string(),
25575
24957
  scopes: z.array(TokenScopeSchema),
25576
24958
  redirectUri: z.string(),
25577
- hubUrl: z.string()
24959
+ hubUrl: z.string(),
24960
+ /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
24961
+ * that carries one can ONLY be exchanged with the matching verifier. */
24962
+ codeChallenge: z.string().optional()
25578
24963
  }), z.object({ code: z.string() }), {
25579
24964
  kind: "mutation",
25580
24965
  access: "create"
25581
24966
  }),
25582
24967
  oauthExchangeCode: method(z.object({
25583
24968
  code: z.string(),
25584
- redirectUri: z.string()
24969
+ redirectUri: z.string(),
24970
+ /** PKCE verifier. REQUIRED when the code carries a challenge. */
24971
+ codeVerifier: z.string().optional()
25585
24972
  }), z.object({
25586
24973
  accessToken: z.string(),
25587
24974
  refreshToken: z.string(),
@@ -26123,277 +25510,872 @@ var zoneAnalyticsCapability = {
26123
25510
  runtimeState: CameraOccupancySnapshotSchema
26124
25511
  };
26125
25512
  /**
26126
- * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
26127
- * cap so a single CRUD surface backs every consumer; each stage has
26128
- * its own dev-state mirror slice (`motion-zone-rules`,
26129
- * `detection-zone-rules`, …) so consumer addons subscribe independently.
25513
+ * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
25514
+ * cap so a single CRUD surface backs every consumer; each stage has
25515
+ * its own dev-state mirror slice (`motion-zone-rules`,
25516
+ * `detection-zone-rules`, …) so consumer addons subscribe independently.
25517
+ *
25518
+ * Extend the enum here when a new gating consumer comes online (audio
25519
+ * gating, alert filtering, …) — no other surface needs to change.
25520
+ */
25521
+ var ZoneRuleStageEnum = z.enum([
25522
+ "motion",
25523
+ "detection",
25524
+ "package"
25525
+ ]);
25526
+ /**
25527
+ * Zone rules capability — per-camera CRUD over the {@link ZoneRule}
25528
+ * arrays that decide how each pipeline stage uses the polygon zones.
25529
+ *
25530
+ * Hosted by `addon-pipeline-orchestrator` alongside the zones provider
25531
+ * so the operator has a single hub-side source of truth for both
25532
+ * geometry and behaviour. Per-stage rules are stored under the
25533
+ * `zoneRules.<stage>` key in the orchestrator's per-device store and
25534
+ * mirrored to the device-state slice `<stage>-zone-rules` on every
25535
+ * mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
25536
+ * subscribe to that slice and refresh their gating without
25537
+ * round-tripping the cap.
25538
+ *
25539
+ * Sets are bulk-replace — the operator UI sends the new rule list
25540
+ * wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
25541
+ * naturally in the rule editor without per-rule mutation chatter.
25542
+ */
25543
+ var zoneRulesCapability = {
25544
+ name: "zone-rules",
25545
+ scope: "device",
25546
+ mode: "singleton",
25547
+ deviceTypes: [DeviceType.Camera],
25548
+ methods: {
25549
+ /** Read the full rule list for a given stage (empty when no rules
25550
+ * are defined yet). */
25551
+ listRules: method(z.object({
25552
+ deviceId: z.number(),
25553
+ stage: ZoneRuleStageEnum
25554
+ }), z.array(ZoneRuleSchema).readonly()),
25555
+ /** Bulk-replace the rule list for one stage. The provider validates
25556
+ * each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
25557
+ * thresholds in range) and rejects the whole patch if any entry
25558
+ * is invalid — partial writes are a configuration footgun. */
25559
+ setRules: method(z.object({
25560
+ deviceId: z.number(),
25561
+ stage: ZoneRuleStageEnum,
25562
+ rules: z.array(ZoneRuleSchema).readonly()
25563
+ }), z.void(), {
25564
+ kind: "mutation",
25565
+ auth: "admin"
25566
+ })
25567
+ },
25568
+ /**
25569
+ * Runtime-state slice — every stage mirrored together so consumers
25570
+ * see one reactive handle (`device.state.zoneRules.value`) instead
25571
+ * of one per stage. Bulk-replace mutations on any stage write the full
25572
+ * `{motion, detection, package}` shape, so subscribers always get the
25573
+ * complete current set. Consumers that only care about one stage
25574
+ * just read the matching property.
25575
+ *
25576
+ * `package` backs the package-drop detector — a package zone is a
25577
+ * `ZoneRule` on the `'package'` stage referencing drawn polygons
25578
+ * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
25579
+ * The orchestrator provider writes this stage as a first-class slice
25580
+ * (Phase 4): every mutation mirrors the full `{motion, detection,
25581
+ * package}` shape, so consumers read the current package rules directly
25582
+ * off `device.state.zoneRules.value.package`.
25583
+ */
25584
+ runtimeState: z.object({
25585
+ motion: z.array(ZoneRuleSchema).readonly(),
25586
+ detection: z.array(ZoneRuleSchema).readonly(),
25587
+ package: z.array(ZoneRuleSchema).readonly()
25588
+ })
25589
+ };
25590
+ /**
25591
+ * Runtime defaults -- used by ConfigManager.get() for backward compatibility
25592
+ * until Plan B wires all runtime settings to the system_settings SQL table.
25593
+ *
25594
+ * Moved from @camstack/system/config-schema to @camstack/types so that
25595
+ * @camstack/system can reference it without a cross-package kernel import.
25596
+ */
25597
+ var RUNTIME_DEFAULTS = {
25598
+ "features.streaming": true,
25599
+ "features.notifications": true,
25600
+ "features.objectDetection": false,
25601
+ "features.remoteAccess": true,
25602
+ "features.agentCluster": false,
25603
+ "features.smartHome": true,
25604
+ "features.recordings": true,
25605
+ "features.backup": true,
25606
+ "features.repl": true,
25607
+ "retention.detectionEventsDays": 30,
25608
+ "retention.audioLevelsDays": 7,
25609
+ "logging.level": "info",
25610
+ "logging.retentionDays": 30,
25611
+ "eventBus.ringBufferSize": 1e4,
25612
+ "storage.provider": "sqlite-storage",
25613
+ "storage.locations": {
25614
+ data: "camstack-data/data",
25615
+ media: "camstack-data/media",
25616
+ recordings: "camstack-data/recordings",
25617
+ cache: "/tmp/camstack-cache",
25618
+ logs: "camstack-data/logs",
25619
+ models: "camstack-data/models"
25620
+ },
25621
+ providers: [],
25622
+ "ffmpeg.binaryPath": "ffmpeg",
25623
+ "ffmpeg.hwAccel": "auto",
25624
+ "ffmpeg.threadCount": 0,
25625
+ "auth.tokenExpiry": "7d"
25626
+ };
25627
+ /**
25628
+ * Accessory device helpers — shared across drivers.
26130
25629
  *
26131
- * Extend the enum here when a new gating consumer comes online (audio
26132
- * gating, alert filtering, …) no other surface needs to change.
25630
+ * Many vendor-specific drivers register accessory child devices on
25631
+ * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
25632
+ * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
25633
+ * driver picks the right `DeviceType` + `DeviceRole` explicitly when
25634
+ * spawning, builds a name derived from the parent, and produces a
25635
+ * stableId tied to the parent so boot-restore can reconstruct the
25636
+ * relationship.
25637
+ *
25638
+ * Centralised `(kind → DeviceType)` mapping was dropped on purpose:
25639
+ * drivers may reasonably disagree on the right type for an accessory
25640
+ * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
25641
+ * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
25642
+ * one canonical mapping was over-prescriptive and added a layer of
25643
+ * indirection without saving meaningful code at call sites — the
25644
+ * driver knows its own hardware best.
26133
25645
  */
26134
- var ZoneRuleStageEnum = z.enum([
26135
- "motion",
26136
- "detection",
26137
- "package"
26138
- ]);
26139
25646
  /**
26140
- * Zone rules capability per-camera CRUD over the {@link ZoneRule}
26141
- * arrays that decide how each pipeline stage uses the polygon zones.
25647
+ * Subset of `DeviceRole` values that drivers register as child
25648
+ * accessories of a parent device. Sourced verbatim from `DeviceRole`
25649
+ * — `AccessoryKind` is the alias drivers use when building accessory
25650
+ * children, so the call site reads as
25651
+ * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
25652
+ * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
25653
+ * any role works, including non-accessory ones like Doorbell).
25654
+ */
25655
+ var AccessoryKind = {
25656
+ Siren: DeviceRole.Siren,
25657
+ Floodlight: DeviceRole.Floodlight,
25658
+ Spotlight: DeviceRole.Spotlight,
25659
+ PirSensor: DeviceRole.PirSensor,
25660
+ Chime: DeviceRole.Chime,
25661
+ Autotrack: DeviceRole.Autotrack,
25662
+ Nightvision: DeviceRole.Nightvision,
25663
+ PrivacyMask: DeviceRole.PrivacyMask
25664
+ };
25665
+ AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
25666
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
25667
+ new Set(Object.values(DeviceType));
25668
+ DeviceFeature.BatteryOperated;
25669
+ /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
25670
+ * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
25671
+ function unwrap(schema) {
25672
+ let s = schema;
25673
+ while (s instanceof z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
25674
+ return s;
25675
+ }
25676
+ function leafKind(schema) {
25677
+ const s = unwrap(schema);
25678
+ if (s instanceof z.ZodEnum) return {
25679
+ kind: "enum",
25680
+ enumValues: s.options
25681
+ };
25682
+ if (s instanceof z.ZodNumber) return { kind: "number" };
25683
+ if (s instanceof z.ZodString) return { kind: "string" };
25684
+ if (s instanceof z.ZodBoolean) return { kind: "boolean" };
25685
+ return null;
25686
+ }
25687
+ /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
25688
+ * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
25689
+ * so fields with null live values are still offered. Arrays / unknown shapes
25690
+ * are skipped — never throws. */
25691
+ function enumerateSchemaFields(schema, prefix = "") {
25692
+ const root = unwrap(schema);
25693
+ if (!(root instanceof z.ZodObject)) return [];
25694
+ const out = [];
25695
+ const shape = root.shape;
25696
+ for (const [key, field] of Object.entries(shape)) {
25697
+ const path = prefix ? `${prefix}.${key}` : key;
25698
+ const inner = unwrap(field);
25699
+ if (inner instanceof z.ZodObject) {
25700
+ out.push(...enumerateSchemaFields(inner, path));
25701
+ continue;
25702
+ }
25703
+ const leaf = leafKind(field);
25704
+ if (leaf) out.push({
25705
+ path,
25706
+ kind: leaf.kind,
25707
+ ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
25708
+ });
25709
+ }
25710
+ return out;
25711
+ }
25712
+ /** Enumerate the per-item wireable fields of an item-array cap (see
25713
+ * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
25714
+ * `keyField` (the key comes from the link's `itemKey`, never from a wired
25715
+ * source), each tagged `item: true` so the authoring UI collects an
25716
+ * `itemKey` alongside the field. Never throws. */
25717
+ function enumerateItemArrayFields(itemArray) {
25718
+ return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
25719
+ ...f,
25720
+ item: true
25721
+ }));
25722
+ }
25723
+ /**
25724
+ * Error types for the safe expression engine. Two distinct classes so callers
25725
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
25726
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
25727
+ */
25728
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
25729
+ * the failure is anchored to a character (author-facing inline feedback). */
25730
+ var ExpressionParseError = class extends Error {
25731
+ position;
25732
+ constructor(message, position) {
25733
+ super(message);
25734
+ this.name = "ExpressionParseError";
25735
+ this.position = position;
25736
+ }
25737
+ };
25738
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
25739
+ * result, unknown builtin, step-budget exceeded). */
25740
+ var ExpressionEvalError = class extends Error {
25741
+ constructor(message) {
25742
+ super(message);
25743
+ this.name = "ExpressionEvalError";
25744
+ }
25745
+ };
25746
+ /**
25747
+ * Frozen, null-prototype builtin function table for the expression engine
25748
+ * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
25749
+ * parser rejects any callee not in it, and the evaluator gates each call on an
25750
+ * own-property check against it.
26142
25751
  *
26143
- * Hosted by `addon-pipeline-orchestrator` alongside the zones provider
26144
- * so the operator has a single hub-side source of truth for both
26145
- * geometry and behaviour. Per-stage rules are stored under the
26146
- * `zoneRules.<stage>` key in the orchestrator's per-device store and
26147
- * mirrored to the device-state slice `<stage>-zone-rules` on every
26148
- * mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
26149
- * subscribe to that slice and refresh their gating without
26150
- * round-tripping the cap.
25752
+ * Because the object has a NULL prototype AND is `Object.freeze`d:
25753
+ * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
25754
+ * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
25755
+ * (there is no `Object.prototype` in the chain), so those names are not
25756
+ * callable they are simply "unknown function" at parse time.
26151
25757
  *
26152
- * Sets are bulk-replace the operator UI sends the new rule list
26153
- * wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
26154
- * naturally in the rule editor without per-rule mutation chatter.
25758
+ * Every numeric argument is validated as a finite number and every numeric
25759
+ * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
25760
+ * (`pow(10,400)` Infinity) all raise `ExpressionEvalError` and fail the link
25761
+ * closed rather than emitting a garbage value.
26155
25762
  */
26156
- var zoneRulesCapability = {
26157
- name: "zone-rules",
26158
- scope: "device",
26159
- mode: "singleton",
26160
- deviceTypes: [DeviceType.Camera],
26161
- methods: {
26162
- /** Read the full rule list for a given stage (empty when no rules
26163
- * are defined yet). */
26164
- listRules: method(z.object({
26165
- deviceId: z.number(),
26166
- stage: ZoneRuleStageEnum
26167
- }), z.array(ZoneRuleSchema).readonly()),
26168
- /** Bulk-replace the rule list for one stage. The provider validates
26169
- * each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
26170
- * thresholds in range) and rejects the whole patch if any entry
26171
- * is invalid partial writes are a configuration footgun. */
26172
- setRules: method(z.object({
26173
- deviceId: z.number(),
26174
- stage: ZoneRuleStageEnum,
26175
- rules: z.array(ZoneRuleSchema).readonly()
26176
- }), z.void(), {
26177
- kind: "mutation",
26178
- auth: "admin"
26179
- })
25763
+ function asFiniteNumber(value, name, index) {
25764
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
25765
+ return value;
25766
+ }
25767
+ function asString$1(value, name, index) {
25768
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
25769
+ return value;
25770
+ }
25771
+ function finiteResult(value, name) {
25772
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
25773
+ return value;
25774
+ }
25775
+ function allFiniteNumbers(args, name) {
25776
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
25777
+ }
25778
+ var INF = Number.POSITIVE_INFINITY;
25779
+ var table = {
25780
+ min: {
25781
+ minArgs: 1,
25782
+ maxArgs: INF,
25783
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
25784
+ },
25785
+ max: {
25786
+ minArgs: 1,
25787
+ maxArgs: INF,
25788
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
25789
+ },
25790
+ abs: {
25791
+ minArgs: 1,
25792
+ maxArgs: 1,
25793
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
25794
+ },
25795
+ floor: {
25796
+ minArgs: 1,
25797
+ maxArgs: 1,
25798
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
25799
+ },
25800
+ ceil: {
25801
+ minArgs: 1,
25802
+ maxArgs: 1,
25803
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
25804
+ },
25805
+ sqrt: {
25806
+ minArgs: 1,
25807
+ maxArgs: 1,
25808
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
25809
+ },
25810
+ round: {
25811
+ minArgs: 1,
25812
+ maxArgs: 2,
25813
+ apply: (args) => {
25814
+ const x = asFiniteNumber(args[0], "round", 0);
25815
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
25816
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
25817
+ const factor = 10 ** digits;
25818
+ return finiteResult(Math.round(x * factor) / factor, "round");
25819
+ }
25820
+ },
25821
+ pow: {
25822
+ minArgs: 2,
25823
+ maxArgs: 2,
25824
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
25825
+ },
25826
+ clamp: {
25827
+ minArgs: 3,
25828
+ maxArgs: 3,
25829
+ apply: (args) => {
25830
+ const x = asFiniteNumber(args[0], "clamp", 0);
25831
+ const lo = asFiniteNumber(args[1], "clamp", 1);
25832
+ const hi = asFiniteNumber(args[2], "clamp", 2);
25833
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
25834
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
25835
+ }
25836
+ },
25837
+ avg: {
25838
+ minArgs: 1,
25839
+ maxArgs: INF,
25840
+ apply: (args) => {
25841
+ const nums = allFiniteNumbers(args, "avg");
25842
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
25843
+ }
26180
25844
  },
26181
- /**
26182
- * Runtime-state slice — every stage mirrored together so consumers
26183
- * see one reactive handle (`device.state.zoneRules.value`) instead
26184
- * of one per stage. Bulk-replace mutations on any stage write the full
26185
- * `{motion, detection, package}` shape, so subscribers always get the
26186
- * complete current set. Consumers that only care about one stage
26187
- * just read the matching property.
26188
- *
26189
- * `package` backs the package-drop detector — a package zone is a
26190
- * `ZoneRule` on the `'package'` stage referencing drawn polygons
26191
- * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
26192
- * The orchestrator provider writes this stage as a first-class slice
26193
- * (Phase 4): every mutation mirrors the full `{motion, detection,
26194
- * package}` shape, so consumers read the current package rules directly
26195
- * off `device.state.zoneRules.value.package`.
26196
- */
26197
- runtimeState: z.object({
26198
- motion: z.array(ZoneRuleSchema).readonly(),
26199
- detection: z.array(ZoneRuleSchema).readonly(),
26200
- package: z.array(ZoneRuleSchema).readonly()
26201
- })
25845
+ sum: {
25846
+ minArgs: 1,
25847
+ maxArgs: INF,
25848
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
25849
+ },
25850
+ coalesce: {
25851
+ minArgs: 1,
25852
+ maxArgs: INF,
25853
+ apply: (args) => {
25854
+ for (const a of args) if (a !== null) return a;
25855
+ return null;
25856
+ }
25857
+ },
25858
+ age: {
25859
+ minArgs: 2,
25860
+ maxArgs: 2,
25861
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
25862
+ },
25863
+ convert: {
25864
+ minArgs: 3,
25865
+ maxArgs: 3,
25866
+ apply: (args, hooks) => {
25867
+ const x = asFiniteNumber(args[0], "convert", 0);
25868
+ const from = asString$1(args[1], "convert", 1).trim();
25869
+ const to = asString$1(args[2], "convert", 2).trim();
25870
+ if (hooks.convert) {
25871
+ const out = hooks.convert(x, from, to);
25872
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
25873
+ return finiteResult(out, "convert");
25874
+ }
25875
+ if (from === to) return x;
25876
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
25877
+ }
25878
+ }
26202
25879
  };
25880
+ Object.freeze(Object.assign(Object.create(null), table));
25881
+ /** The set of valid builtin names — used by the parser to reject unknown
25882
+ * callees at parse time (immediate author feedback). */
25883
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
26203
25884
  /**
26204
- * Runtime defaults -- used by ConfigManager.get() for backward compatibility
26205
- * until Plan B wires all runtime settings to the system_settings SQL table.
25885
+ * Resource-bound constants for the safe expression engine.
26206
25886
  *
26207
- * Moved from @camstack/system/config-schema to @camstack/types so that
26208
- * @camstack/system can reference it without a cross-package kernel import.
25887
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
25888
+ * loops, recursion, lambdas or member access see `ast.ts`), so evaluation is
25889
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
25890
+ * work a single author-supplied expression can request, so a hostile or
25891
+ * accidental pathological string can never spend unbounded CPU/memory.
26209
25892
  */
26210
- var RUNTIME_DEFAULTS = {
26211
- "features.streaming": true,
26212
- "features.notifications": true,
26213
- "features.objectDetection": false,
26214
- "features.remoteAccess": true,
26215
- "features.agentCluster": false,
26216
- "features.smartHome": true,
26217
- "features.recordings": true,
26218
- "features.backup": true,
26219
- "features.repl": true,
26220
- "retention.detectionEventsDays": 30,
26221
- "retention.audioLevelsDays": 7,
26222
- "logging.level": "info",
26223
- "logging.retentionDays": 30,
26224
- "eventBus.ringBufferSize": 1e4,
26225
- "storage.provider": "sqlite-storage",
26226
- "storage.locations": {
26227
- data: "camstack-data/data",
26228
- media: "camstack-data/media",
26229
- recordings: "camstack-data/recordings",
26230
- cache: "/tmp/camstack-cache",
26231
- logs: "camstack-data/logs",
26232
- models: "camstack-data/models"
26233
- },
26234
- providers: [],
26235
- "ffmpeg.binaryPath": "ffmpeg",
26236
- "ffmpeg.hwAccel": "auto",
26237
- "ffmpeg.threadCount": 0,
26238
- "auth.tokenExpiry": "7d"
26239
- };
25893
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
25894
+ * rejected without allocation. */
25895
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
25896
+ /** A legal binding / identifier name. */
25897
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
25898
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
25899
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
25900
+ var RESERVED_BINDING_NAMES = new Set([
25901
+ "now",
25902
+ "true",
25903
+ "false",
25904
+ "null"
25905
+ ]);
26240
25906
  /**
26241
- * Accessory device helpers shared across drivers.
25907
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
25908
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
25909
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
25910
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
25911
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
25912
+ * is a parse error with a source position, so member access / assignment /
25913
+ * template literals are lexically impossible.
25914
+ */
25915
+ var KEYWORDS = new Set([
25916
+ "true",
25917
+ "false",
25918
+ "null"
25919
+ ]);
25920
+ function isDigit(ch) {
25921
+ return ch >= "0" && ch <= "9";
25922
+ }
25923
+ function isIdentStart(ch) {
25924
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
25925
+ }
25926
+ function isIdentPart(ch) {
25927
+ return isIdentStart(ch) || isDigit(ch);
25928
+ }
25929
+ function isWhitespace(ch) {
25930
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
25931
+ }
25932
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
25933
+ * Throws `ExpressionParseError` on any illegal character or unterminated
25934
+ * string. */
25935
+ function tokenize(source) {
25936
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
25937
+ const tokens = [];
25938
+ let i = 0;
25939
+ const n = source.length;
25940
+ while (i < n) {
25941
+ const ch = source[i];
25942
+ if (isWhitespace(ch)) {
25943
+ i += 1;
25944
+ continue;
25945
+ }
25946
+ if (isDigit(ch)) {
25947
+ const start = i;
25948
+ while (i < n && isDigit(source[i])) i += 1;
25949
+ if (i < n && source[i] === ".") {
25950
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
25951
+ i += 1;
25952
+ while (i < n && isDigit(source[i])) i += 1;
25953
+ }
25954
+ const text = source.slice(start, i);
25955
+ const value = Number(text);
25956
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
25957
+ tokens.push({
25958
+ type: "number",
25959
+ value,
25960
+ pos: start
25961
+ });
25962
+ continue;
25963
+ }
25964
+ if (ch === "'" || ch === "\"") {
25965
+ const quote = ch;
25966
+ const start = i;
25967
+ i += 1;
25968
+ let out = "";
25969
+ let closed = false;
25970
+ while (i < n) {
25971
+ const c = source[i];
25972
+ if (c === "\\") {
25973
+ const next = i + 1 < n ? source[i + 1] : "";
25974
+ if (next === "\\" || next === "'" || next === "\"") {
25975
+ out += next;
25976
+ i += 2;
25977
+ continue;
25978
+ }
25979
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
25980
+ }
25981
+ if (c === quote) {
25982
+ closed = true;
25983
+ i += 1;
25984
+ break;
25985
+ }
25986
+ out += c;
25987
+ i += 1;
25988
+ }
25989
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
25990
+ tokens.push({
25991
+ type: "string",
25992
+ value: out,
25993
+ pos: start
25994
+ });
25995
+ continue;
25996
+ }
25997
+ if (isIdentStart(ch)) {
25998
+ const start = i;
25999
+ while (i < n && isIdentPart(source[i])) i += 1;
26000
+ const text = source.slice(start, i);
26001
+ if (KEYWORDS.has(text)) tokens.push({
26002
+ type: "keyword",
26003
+ keyword: keywordOf(text),
26004
+ pos: start
26005
+ });
26006
+ else tokens.push({
26007
+ type: "identifier",
26008
+ name: text,
26009
+ pos: start
26010
+ });
26011
+ continue;
26012
+ }
26013
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
26014
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
26015
+ tokens.push({
26016
+ type: "punct",
26017
+ punct: two,
26018
+ pos: i
26019
+ });
26020
+ i += 2;
26021
+ continue;
26022
+ }
26023
+ if (isSinglePunct(ch)) {
26024
+ tokens.push({
26025
+ type: "punct",
26026
+ punct: ch,
26027
+ pos: i
26028
+ });
26029
+ i += 1;
26030
+ continue;
26031
+ }
26032
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
26033
+ }
26034
+ tokens.push({
26035
+ type: "eof",
26036
+ pos: n
26037
+ });
26038
+ return tokens;
26039
+ }
26040
+ function keywordOf(text) {
26041
+ if (text === "true") return "true";
26042
+ if (text === "false") return "false";
26043
+ return "null";
26044
+ }
26045
+ function isSinglePunct(ch) {
26046
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
26047
+ }
26048
+ /**
26049
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
26242
26050
  *
26243
- * Many vendor-specific drivers register accessory child devices on
26244
- * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
26245
- * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
26246
- * driver picks the right `DeviceType` + `DeviceRole` explicitly when
26247
- * spawning, builds a name derived from the parent, and produces a
26248
- * stableId tied to the parent so boot-restore can reconstruct the
26249
- * relationship.
26051
+ * Precedence (low → high): ternary `?:` (right-assoc) `||` `&&` equality
26052
+ * relational additive multiplicative unary `! -` call / primary.
26053
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
26054
+ * string validated against the builtin table at parse time, so an unknown
26055
+ * function is rejected immediately (author feedback) and a persisted expression
26056
+ * that references a since-removed builtin degrades at read.
26250
26057
  *
26251
- * Centralised `(kind DeviceType)` mapping was dropped on purpose:
26252
- * drivers may reasonably disagree on the right type for an accessory
26253
- * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
26254
- * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
26255
- * one canonical mapping was over-prescriptive and added a layer of
26256
- * indirection without saving meaningful code at call sites — the
26257
- * driver knows its own hardware best.
26058
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
26059
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) both raise `ExpressionParseError`.
26258
26060
  */
26061
+ /** Binary/logical operator precedence (higher binds tighter). */
26062
+ var BINARY_PRECEDENCE = {
26063
+ "||": 1,
26064
+ "&&": 2,
26065
+ "==": 3,
26066
+ "!=": 3,
26067
+ "<": 4,
26068
+ "<=": 4,
26069
+ ">": 4,
26070
+ ">=": 4,
26071
+ "+": 5,
26072
+ "-": 5,
26073
+ "*": 6,
26074
+ "/": 6,
26075
+ "%": 6
26076
+ };
26077
+ function isLogicalOp(op) {
26078
+ return op === "&&" || op === "||";
26079
+ }
26080
+ function isBinaryOp(op) {
26081
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
26082
+ }
26083
+ var Parser = class {
26084
+ tokens;
26085
+ pos = 0;
26086
+ nodeCount = 0;
26087
+ identifiers = /* @__PURE__ */ new Set();
26088
+ callees = /* @__PURE__ */ new Set();
26089
+ constructor(tokens) {
26090
+ this.tokens = tokens;
26091
+ }
26092
+ parse() {
26093
+ const ast = this.parseTernary();
26094
+ const tok = this.peek();
26095
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
26096
+ return {
26097
+ ast,
26098
+ identifiers: this.identifiers,
26099
+ callees: this.callees,
26100
+ nodeCount: this.nodeCount
26101
+ };
26102
+ }
26103
+ peek() {
26104
+ return this.tokens[this.pos];
26105
+ }
26106
+ next() {
26107
+ return this.tokens[this.pos++];
26108
+ }
26109
+ /** Consume a punctuator token, erroring if the next token isn't it. */
26110
+ expectPunct(punct) {
26111
+ const tok = this.peek();
26112
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
26113
+ this.pos += 1;
26114
+ }
26115
+ matchPunct(punct) {
26116
+ const tok = this.peek();
26117
+ if (tok.type === "punct" && tok.punct === punct) {
26118
+ this.pos += 1;
26119
+ return true;
26120
+ }
26121
+ return false;
26122
+ }
26123
+ countNode() {
26124
+ this.nodeCount += 1;
26125
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
26126
+ }
26127
+ parseTernary() {
26128
+ const test = this.parseBinary(1);
26129
+ if (this.matchPunct("?")) {
26130
+ const consequent = this.parseTernary();
26131
+ this.expectPunct(":");
26132
+ const alternate = this.parseTernary();
26133
+ this.countNode();
26134
+ return {
26135
+ kind: "conditional",
26136
+ test,
26137
+ consequent,
26138
+ alternate
26139
+ };
26140
+ }
26141
+ return test;
26142
+ }
26143
+ parseBinary(minPrec) {
26144
+ let left = this.parseUnary();
26145
+ for (;;) {
26146
+ const tok = this.peek();
26147
+ if (tok.type !== "punct") break;
26148
+ const prec = BINARY_PRECEDENCE[tok.punct];
26149
+ if (prec === void 0 || prec < minPrec) break;
26150
+ const op = tok.punct;
26151
+ this.pos += 1;
26152
+ const right = this.parseBinary(prec + 1);
26153
+ this.countNode();
26154
+ if (isLogicalOp(op)) left = {
26155
+ kind: "logical",
26156
+ op,
26157
+ left,
26158
+ right
26159
+ };
26160
+ else if (isBinaryOp(op)) left = {
26161
+ kind: "binary",
26162
+ op,
26163
+ left,
26164
+ right
26165
+ };
26166
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
26167
+ }
26168
+ return left;
26169
+ }
26170
+ parseUnary() {
26171
+ const tok = this.peek();
26172
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
26173
+ const op = tok.punct;
26174
+ this.pos += 1;
26175
+ const operand = this.parseUnary();
26176
+ this.countNode();
26177
+ return {
26178
+ kind: "unary",
26179
+ op,
26180
+ operand
26181
+ };
26182
+ }
26183
+ return this.parsePrimary();
26184
+ }
26185
+ parsePrimary() {
26186
+ const tok = this.next();
26187
+ switch (tok.type) {
26188
+ case "number":
26189
+ this.countNode();
26190
+ return {
26191
+ kind: "literal",
26192
+ value: tok.value
26193
+ };
26194
+ case "string":
26195
+ this.countNode();
26196
+ return {
26197
+ kind: "literal",
26198
+ value: tok.value
26199
+ };
26200
+ case "keyword":
26201
+ this.countNode();
26202
+ return {
26203
+ kind: "literal",
26204
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
26205
+ };
26206
+ case "identifier": {
26207
+ const nextTok = this.peek();
26208
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
26209
+ this.identifiers.add(tok.name);
26210
+ this.countNode();
26211
+ return {
26212
+ kind: "identifier",
26213
+ name: tok.name
26214
+ };
26215
+ }
26216
+ case "punct":
26217
+ if (tok.punct === "(") {
26218
+ const inner = this.parseTernary();
26219
+ this.expectPunct(")");
26220
+ return inner;
26221
+ }
26222
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
26223
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
26224
+ }
26225
+ }
26226
+ parseCall(callee, pos) {
26227
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
26228
+ this.expectPunct("(");
26229
+ const args = [];
26230
+ if (!this.matchPunct(")")) for (;;) {
26231
+ args.push(this.parseTernary());
26232
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
26233
+ if (this.matchPunct(",")) continue;
26234
+ this.expectPunct(")");
26235
+ break;
26236
+ }
26237
+ this.callees.add(callee);
26238
+ this.countNode();
26239
+ return {
26240
+ kind: "call",
26241
+ callee,
26242
+ args
26243
+ };
26244
+ }
26245
+ };
26246
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
26247
+ * `ExpressionParseError` on any lexical or grammatical failure. */
26248
+ function parseExpression(source) {
26249
+ return new Parser(tokenize(source)).parse();
26250
+ }
26259
26251
  /**
26260
- * Subset of `DeviceRole` values that drivers register as child
26261
- * accessories of a parent device. Sourced verbatim from `DeviceRole`
26262
- * `AccessoryKind` is the alias drivers use when building accessory
26263
- * children, so the call site reads as
26264
- * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
26265
- * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
26266
- * any role works, including non-accessory ones like Doorbell).
26252
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
26253
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
26254
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
26255
+ * one per read on a hot resolve path.
26256
+ *
26257
+ * The cache is a module-level singleton: entries are pure, content-addressed
26258
+ * ASTs keyed by the raw source string, so sharing one instance across all
26259
+ * callers is safe and maximises hit rate.
26267
26260
  */
26268
- var AccessoryKind = {
26269
- Siren: DeviceRole.Siren,
26270
- Floodlight: DeviceRole.Floodlight,
26271
- Spotlight: DeviceRole.Spotlight,
26272
- PirSensor: DeviceRole.PirSensor,
26273
- Chime: DeviceRole.Chime,
26274
- Autotrack: DeviceRole.Autotrack,
26275
- Nightvision: DeviceRole.Nightvision,
26276
- PrivacyMask: DeviceRole.PrivacyMask
26277
- };
26278
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
26279
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
26280
- new Set(Object.values(DeviceType));
26281
- /** Apply a link transform to a resolved source value. `identity`/undefined
26282
- * pass through; `enum-map` remaps strings (fallback or null when unknown);
26283
- * `linear` does scale*x+offset with optional clamp (null for non-numbers). */
26284
- function applyTransform(value, transform) {
26285
- if (!transform || transform.kind === "identity") return value;
26286
- if (transform.kind === "enum-map") {
26287
- const key = String(value);
26288
- if (Object.prototype.hasOwnProperty.call(transform.mapping, key)) return transform.mapping[key];
26289
- return transform.fallback ?? null;
26261
+ var cache = /* @__PURE__ */ new Map();
26262
+ function getCached(source) {
26263
+ const hit = cache.get(source);
26264
+ if (hit !== void 0) {
26265
+ cache.delete(source);
26266
+ cache.set(source, hit);
26267
+ return hit;
26268
+ }
26269
+ let result;
26270
+ try {
26271
+ result = {
26272
+ ok: true,
26273
+ parsed: parseExpression(source)
26274
+ };
26275
+ } catch (err) {
26276
+ result = {
26277
+ ok: false,
26278
+ error: err instanceof ExpressionParseError ? err.message : String(err)
26279
+ };
26290
26280
  }
26291
- if (transform.kind === "linear") {
26292
- if (typeof value !== "number" || Number.isNaN(value)) return null;
26293
- let out = value * transform.scale + transform.offset;
26294
- if (transform.clamp) {
26295
- const min = transform.clamp[0];
26296
- const max = transform.clamp[1];
26297
- out = Math.min(max, Math.max(min, out));
26298
- }
26299
- return out;
26281
+ cache.set(source, result);
26282
+ if (cache.size > 256) {
26283
+ const oldest = cache.keys().next().value;
26284
+ if (oldest !== void 0) cache.delete(oldest);
26300
26285
  }
26301
- return value;
26286
+ return result;
26302
26287
  }
26303
- DeviceFeature.BatteryOperated;
26304
- /** Returns true when `x` is a non-null, non-array plain object. */
26305
- function isRecord(x) {
26306
- return x !== null && typeof x === "object" && !Array.isArray(x);
26288
+ /** Compile `source`, returning a discriminated result instead of throwing.
26289
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
26290
+ function compileExpressionSafe(source) {
26291
+ return getCached(source);
26307
26292
  }
26308
- /** Read a dot-path (`a.b.c`) out of an object. Returns `undefined` for any
26309
- * missing/non-object segment — never throws. */
26310
- function getByPath(obj, path) {
26311
- const parts = path.split(".");
26312
- let cur = obj;
26313
- for (const part of parts) {
26314
- if (!isRecord(cur)) return void 0;
26315
- cur = cur[part];
26293
+ Object.freeze({});
26294
+ /**
26295
+ * Author-time validation. Returns `null` when the source is valid, else a
26296
+ * human-readable error message. Checks: the expression compiles; binding count
26297
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
26298
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
26299
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
26300
+ */
26301
+ function validateExpressionSource(src) {
26302
+ const names = Object.keys(src.bindings);
26303
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
26304
+ for (const name of names) {
26305
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
26306
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
26307
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
26316
26308
  }
26317
- return cur;
26318
- }
26319
- /** Immutably set a dot-path on an object, returning a new object. Intermediate
26320
- * objects are created when a segment is missing or null. Arrays are NOT
26321
- * traversed (P1 has no array-index syntax — see `itemKey` grouping in P2): if a
26322
- * segment resolves to an array it is replaced by a fresh object, so the array
26323
- * is discarded — the only caller (`mergeLinkedStatus`) re-validates the result
26324
- * with the target cap's Zod schema and rejects a malformed overlay.
26325
- * The return type `T` preserves the OUTER shape; the rewritten key is not
26326
- * narrowed, so callers should re-validate against the status schema. */
26327
- function setByPath(obj, path, value) {
26328
- const dotIndex = path.indexOf(".");
26329
- if (dotIndex === -1) return {
26330
- ...obj,
26331
- [path]: value
26332
- };
26333
- const head = path.slice(0, dotIndex);
26334
- const tail = path.slice(dotIndex + 1);
26335
- const child = obj[head];
26336
- const childObj = isRecord(child) ? child : {};
26337
- return {
26338
- ...obj,
26339
- [head]: setByPath(childObj, tail, value)
26340
- };
26341
- }
26342
- /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
26343
- * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
26344
- function unwrap(schema) {
26345
- let s = schema;
26346
- while (s instanceof z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
26347
- return s;
26348
- }
26349
- function leafKind(schema) {
26350
- const s = unwrap(schema);
26351
- if (s instanceof z.ZodEnum) return {
26352
- kind: "enum",
26353
- enumValues: s.options
26354
- };
26355
- if (s instanceof z.ZodNumber) return { kind: "number" };
26356
- if (s instanceof z.ZodString) return { kind: "string" };
26357
- if (s instanceof z.ZodBoolean) return { kind: "boolean" };
26358
- return null;
26359
- }
26360
- /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
26361
- * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
26362
- * so fields with null live values are still offered. Arrays / unknown shapes
26363
- * are skipped — never throws. */
26364
- function enumerateSchemaFields(schema, prefix = "") {
26365
- const root = unwrap(schema);
26366
- if (!(root instanceof z.ZodObject)) return [];
26367
- const out = [];
26368
- const shape = root.shape;
26369
- for (const [key, field] of Object.entries(shape)) {
26370
- const path = prefix ? `${prefix}.${key}` : key;
26371
- const inner = unwrap(field);
26372
- if (inner instanceof z.ZodObject) {
26373
- out.push(...enumerateSchemaFields(inner, path));
26374
- continue;
26375
- }
26376
- const leaf = leafKind(field);
26377
- if (leaf) out.push({
26378
- path,
26379
- kind: leaf.kind,
26380
- ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
26381
- });
26309
+ const compiled = compileExpressionSafe(src.expr);
26310
+ if (!compiled.ok) return compiled.error;
26311
+ const bound = new Set(names);
26312
+ for (const id of compiled.parsed.identifiers) {
26313
+ if (id === "now") continue;
26314
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
26382
26315
  }
26383
- return out;
26384
- }
26385
- /** Enumerate the per-item wireable fields of an item-array cap (see
26386
- * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
26387
- * `keyField` (the key comes from the link's `itemKey`, never from a wired
26388
- * source), each tagged `item: true` so the authoring UI collects an
26389
- * `itemKey` alongside the field. Never throws. */
26390
- function enumerateItemArrayFields(itemArray) {
26391
- return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
26392
- ...f,
26393
- item: true
26394
- }));
26316
+ return null;
26395
26317
  }
26396
26318
  /**
26319
+ * What an expression's named bindings READ from.
26320
+ *
26321
+ * Salvaged verbatim from the deleted device-link mechanism. Wiring's source
26322
+ * kinds were the one part of it worth keeping: addressing a device field by
26323
+ * re-sync-stable `stableId`, a per-device constant, and a sibling-accessory
26324
+ * read are the vocabulary any cross-device derivation needs, and they were
26325
+ * already correct. What wiring got wrong was the DESTINATION — a field on
26326
+ * somebody else's device, with no identity — not the source.
26327
+ *
26328
+ * These shapes are therefore kept, re-homed next to the engine that consumes
26329
+ * them, and are the binding type of a composition recipe (the source picker
26330
+ * stays `deviceManager.getWireableFields`). They deliberately do NOT nest: a
26331
+ * binding is a read, never another expression.
26332
+ *
26333
+ * Schemas are authoritative; every type is `z.infer` of one, so a wire shape and
26334
+ * a TypeScript shape cannot drift apart (`scripts/check-schema-type-twins.ts`).
26335
+ */
26336
+ /** Read a sibling accessory's status field, addressed by the sibling's key.
26337
+ * `kind` is optional for wire compatibility — absent means `'field'`. */
26338
+ var ExpressionFieldBindingSchema = z.object({
26339
+ kind: z.literal("field").optional(),
26340
+ sourceKey: z.string(),
26341
+ cap: z.string(),
26342
+ fieldPath: z.string()
26343
+ });
26344
+ /** A constant. No device is read. */
26345
+ var ExpressionLiteralBindingSchema = z.object({
26346
+ kind: z.literal("literal"),
26347
+ value: z.union([
26348
+ z.string(),
26349
+ z.number(),
26350
+ z.boolean(),
26351
+ z.null()
26352
+ ])
26353
+ });
26354
+ /** Read ANY device's status field, addressed by its re-sync-stable `stableId` —
26355
+ * never by numeric id, which a re-adoption reissues. */
26356
+ var ExpressionGlobalBindingSchema = z.object({
26357
+ kind: z.literal("global"),
26358
+ sourceStableId: z.string(),
26359
+ cap: z.string(),
26360
+ fieldPath: z.string()
26361
+ });
26362
+ var ExpressionBindingSourceSchema = z.union([
26363
+ ExpressionFieldBindingSchema,
26364
+ ExpressionLiteralBindingSchema,
26365
+ ExpressionGlobalBindingSchema
26366
+ ]);
26367
+ z.object({
26368
+ expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
26369
+ bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
26370
+ }).superRefine((src, ctx) => {
26371
+ const err = validateExpressionSource(src);
26372
+ if (err !== null) ctx.addIssue({
26373
+ code: "custom",
26374
+ message: err,
26375
+ path: ["expr"]
26376
+ });
26377
+ });
26378
+ /**
26397
26379
  * Runtime list of cap names with status. Used by the settings
26398
26380
  * aggregator to enumerate caps whose `status` should be polled +
26399
26381
  * streamed via `subscribeDeviceStatusAggregate`.
@@ -27449,6 +27431,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
27449
27431
  addonId: null,
27450
27432
  access: "view"
27451
27433
  },
27434
+ "coreBlocks.restart": {
27435
+ capName: "core-blocks",
27436
+ capScope: "system",
27437
+ addonId: null,
27438
+ access: "create"
27439
+ },
27452
27440
  "coreBlocks.setEnabled": {
27453
27441
  capName: "core-blocks",
27454
27442
  capScope: "system",
@@ -28085,12 +28073,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
28085
28073
  addonId: null,
28086
28074
  access: "create"
28087
28075
  },
28088
- "deviceManager.setDeviceLinks": {
28089
- capName: "device-manager",
28090
- capScope: "system",
28091
- addonId: null,
28092
- access: "create"
28093
- },
28094
28076
  "deviceManager.setDisabled": {
28095
28077
  capName: "device-manager",
28096
28078
  capScope: "system",
@@ -31235,6 +31217,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
31235
31217
  addonId: null,
31236
31218
  access: "create"
31237
31219
  },
31220
+ "streamBroker.fetchEventMedia": {
31221
+ capName: "stream-broker",
31222
+ capScope: "system",
31223
+ addonId: null,
31224
+ access: "create"
31225
+ },
31238
31226
  "streamBroker.getAllRtspEntries": {
31239
31227
  capName: "stream-broker",
31240
31228
  capScope: "system",
@@ -31319,6 +31307,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
31319
31307
  addonId: null,
31320
31308
  access: "create"
31321
31309
  },
31310
+ "streamBroker.produceEventMedia": {
31311
+ capName: "stream-broker",
31312
+ capScope: "system",
31313
+ addonId: null,
31314
+ access: "create"
31315
+ },
31322
31316
  "streamBroker.publishCameraStream": {
31323
31317
  capName: "stream-broker",
31324
31318
  capScope: "system",
@@ -32115,60 +32109,6 @@ TimelapseRuleInputSchema.extend({
32115
32109
  createdAt: z.number(),
32116
32110
  updatedAt: z.number()
32117
32111
  });
32118
- /**
32119
- * The addon id as the REGISTRY, the wire and the durable stores spell it.
32120
- *
32121
- * `AddonContext.id` does not agree with itself across the two context
32122
- * factories:
32123
- *
32124
- * - a FORKED addon gets the bare manifest id
32125
- * (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
32126
- * - an addon co-located in hub-main gets it PREFIXED
32127
- * (`server/backend/src/core/addon/addon-registry.service.ts` →
32128
- * ``id: `addon:${addonId}` ``).
32129
- *
32130
- * Everything an addon might compare `ctx.id` AGAINST carries the bare form:
32131
- * `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
32132
- * `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
32133
- *
32134
- * So `entry.providerAddonId === this.ctx.id` is silently, permanently false
32135
- * for a builtin — and only for a builtin, which is why it survives a green
32136
- * suite whose fake supplies the bare id. That is exactly how camera 615's
32137
- * virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
32138
- * binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
32139
- *
32140
- * Route every comparison between `ctx.id` and a registry/store addon id
32141
- * through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
32142
- * enforces it in the processes where the prefix exists.
32143
- */
32144
- /** The prefix the hub-main context factory prepends to the manifest id. */
32145
- var ADDON_ID_PREFIX = "addon:";
32146
- /** The manifest id, whichever spelling of `ctx.id` you were handed. */
32147
- function bareAddonId(id) {
32148
- return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
32149
- }
32150
- /** True when both ids name the same addon, prefixed or not. */
32151
- function isSameAddonId(a, b) {
32152
- return bareAddonId(a) === bareAddonId(b);
32153
- }
32154
- /**
32155
- * Decode base64 little-endian Float32 back into a vector.
32156
- *
32157
- * Throws on a byte length that is not a multiple of 4 — a truncated vector
32158
- * would otherwise silently rank against a shorter one and produce a plausible
32159
- * score, which is worse than an error.
32160
- */
32161
- function decodeVectorBase64(encoded) {
32162
- const buffer = Buffer.from(encoded, "base64");
32163
- if (buffer.byteLength % 4 !== 0) throw new Error(`decodeVectorBase64: ${buffer.byteLength} bytes is not a whole number of Float32 values`);
32164
- const out = new Float32Array(buffer.byteLength / 4);
32165
- for (let i = 0; i < out.length; i += 1) out[i] = buffer.readFloatLE(i * 4);
32166
- return out;
32167
- }
32168
- /** Vector length implied by a base64 payload, without decoding it. */
32169
- function vectorDimFromBase64(encoded) {
32170
- return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
32171
- }
32172
32112
  z.object({
32173
32113
  /**
32174
32114
  * Fraction of the box's own size added on EACH side before cutting.
@@ -32276,6 +32216,60 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
32276
32216
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
32277
32217
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
32278
32218
  /**
32219
+ * The addon id as the REGISTRY, the wire and the durable stores spell it.
32220
+ *
32221
+ * `AddonContext.id` does not agree with itself across the two context
32222
+ * factories:
32223
+ *
32224
+ * - a FORKED addon gets the bare manifest id
32225
+ * (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
32226
+ * - an addon co-located in hub-main gets it PREFIXED
32227
+ * (`server/backend/src/core/addon/addon-registry.service.ts` →
32228
+ * ``id: `addon:${addonId}` ``).
32229
+ *
32230
+ * Everything an addon might compare `ctx.id` AGAINST carries the bare form:
32231
+ * `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
32232
+ * `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
32233
+ *
32234
+ * So `entry.providerAddonId === this.ctx.id` is silently, permanently false
32235
+ * for a builtin — and only for a builtin, which is why it survives a green
32236
+ * suite whose fake supplies the bare id. That is exactly how camera 615's
32237
+ * virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
32238
+ * binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
32239
+ *
32240
+ * Route every comparison between `ctx.id` and a registry/store addon id
32241
+ * through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
32242
+ * enforces it in the processes where the prefix exists.
32243
+ */
32244
+ /** The prefix the hub-main context factory prepends to the manifest id. */
32245
+ var ADDON_ID_PREFIX = "addon:";
32246
+ /** The manifest id, whichever spelling of `ctx.id` you were handed. */
32247
+ function bareAddonId(id) {
32248
+ return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
32249
+ }
32250
+ /** True when both ids name the same addon, prefixed or not. */
32251
+ function isSameAddonId(a, b) {
32252
+ return bareAddonId(a) === bareAddonId(b);
32253
+ }
32254
+ /**
32255
+ * Decode base64 little-endian Float32 back into a vector.
32256
+ *
32257
+ * Throws on a byte length that is not a multiple of 4 — a truncated vector
32258
+ * would otherwise silently rank against a shorter one and produce a plausible
32259
+ * score, which is worse than an error.
32260
+ */
32261
+ function decodeVectorBase64(encoded) {
32262
+ const buffer = Buffer.from(encoded, "base64");
32263
+ if (buffer.byteLength % 4 !== 0) throw new Error(`decodeVectorBase64: ${buffer.byteLength} bytes is not a whole number of Float32 values`);
32264
+ const out = new Float32Array(buffer.byteLength / 4);
32265
+ for (let i = 0; i < out.length; i += 1) out[i] = buffer.readFloatLE(i * 4);
32266
+ return out;
32267
+ }
32268
+ /** Vector length implied by a base64 payload, without decoding it. */
32269
+ function vectorDimFromBase64(encoded) {
32270
+ return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
32271
+ }
32272
+ /**
32279
32273
  * Scores all applicable inference backends for the given hardware.
32280
32274
  *
32281
32275
  * Rules (hardware-driven only — no Python import probes):
@@ -32389,4 +32383,4 @@ function enumerateInferenceDevices(hw) {
32389
32383
  return out;
32390
32384
  }
32391
32385
  //#endregion
32392
- export { procedureAuthKey as $, enumerateInferenceDevices as A, hydrateSchema as At, isSameAddonId as B, coreBlocksCapability as C, asJsonObject as Ct, deviceStateCapability as D, emitDownForOwnedCaps as Dt, deviceManagerCapability as E, createEvent as Et, filesystemBrowseCapability as F, readinessKey as Ft, logDestinationCapability as G, kebabToCamel as H, getByPath as I, resolveCapMount as It, metricsProviderCapability as J, logLevelAtMost as K, isArrayOutputSchema as L, scopeKey as Lt, enumerateSchemaFields as M, nodePin as Mt, evaluateLinkExpression as N, parseJsonObject as Nt, deviceStatusCapability as O, emitReadiness as Ot, extractNestedAddonId as P, parseJsonUnknown as Pt, platformProbeCapability as Q, isCollectionArrayMethod as R, sleep as Rt, buildStreamParamsConfigSchema as S, WELL_KNOWN_TAB_MAP as St, decodeVectorBase64 as T, asString as Tt, lifecycleJobSchema as U, isVoidInput as V, localNetworkCapability as W, objectInputDeclaresAddonId as X, normalizeUnit as Y, parseStreamParamsFormPatch as Z, alertsCapability as _, DeviceFeature as _t, CAP_NAMES_WITH_STATUS as a, storageProviderCapability as at, backupCapability as b, ReadinessRegistry as bt, METHOD_ACCESS_MAP as c, userManagementCapability as ct, ScopedTokenSchema as d, vectorStoreCapability as dt, scoreRuntimes as et, StorageLocationTypeSchema as f, errMsg as ft, addonWidgetsCapability as g, DEVICE_STATUS_METHOD as gt, addonSettingsCapability as h, DEVICE_SETTINGS_CONTRIBUTION_METHODS as ht, BatteryStatusSchema as i, storageCapability as it, enumerateItemArrayFields as j, isDeviceConfigCap as jt, doorbellCapability as k, expandCapMethods as kt, RUNTIME_DEFAULTS as l, validateExpressionSource as lt, addonPagesCapability as m, DATAPLANE_SECRET_HEADER as mt, AlertSchema as n, settingsStoreCapability as nt, CoreBlockSchema as o, streamQualityLabel as ot, UserRecordSchema as p, BaseAddon as pt, looseSchema as q, ApiKeyRecordSchema as r, snapshotCapability as rt, DeviceStatusSchema as s, toExpressionValue as st, ALL_CAPABILITY_DEFINITIONS as t, setByPath as tt, STREAM_PROFILE_META as u, vectorDimFromBase64 as ut, applyTransform as v, DeviceRole as vt, dataStoreProviderCapability as w, asNumber as wt, bareAddonId as x, ReadinessTimeoutError as xt, authProviderCapability as y, DeviceType as yt, isObjectInput as z, EventCategory as zt };
32386
+ export { platformProbeCapability as $, deviceStatusCapability as A, nodePin as At, isSameAddonId as B, coreBlockAddonId as C, asString as Ct, decodeVectorBase64 as D, expandCapMethods as Dt, dataStoreProviderCapability as E, emitReadiness as Et, extractNestedAddonId as F, scopeKey as Ft, logDestinationCapability as G, kebabToCamel as H, filesystemBrowseCapability as I, sleep as It, metricsProviderCapability as J, logLevelAtMost as K, isArrayOutputSchema as L, EventCategory as Lt, enumerateInferenceDevices as M, parseJsonUnknown as Mt, enumerateItemArrayFields as N, readinessKey as Nt, deviceManagerCapability as O, hydrateSchema as Ot, enumerateSchemaFields as P, resolveCapMount as Pt, parseStreamParamsFormPatch as Q, isCollectionArrayMethod as R, buildStreamParamsConfigSchema as S, asNumber as St, coreBlocksCapability as T, emitDownForOwnedCaps as Tt, lifecycleJobSchema as U, isVoidInput as V, localNetworkCapability as W, oauthIntegrationCapability as X, normalizeUnit as Y, objectInputDeclaresAddonId as Z, addonWidgetsCapability as _, DeviceType as _t, CAP_NAMES_WITH_STATUS as a, storageProviderCapability as at, backupCapability as b, WELL_KNOWN_TAB_MAP as bt, DeviceStatusSchema as c, vectorDimFromBase64 as ct, STREAM_PROFILE_META as d, BaseAddon as dt, procedureAuthKey as et, ScopedTokenSchema as f, DATAPLANE_SECRET_HEADER as ft, addonSettingsCapability as g, DeviceRole as gt, addonPagesCapability as h, DeviceFeature as ht, BatteryStatusSchema as i, storageCapability as it, doorbellCapability as j, parseJsonObject as jt, deviceStateCapability as k, isDeviceConfigCap as kt, METHOD_ACCESS_MAP as l, vectorStoreCapability as lt, UserRecordSchema as m, DEVICE_STATUS_METHOD as mt, AlertSchema as n, settingsStoreCapability as nt, CORE_BLOCK_ADDON_PREFIX as o, streamQualityLabel as ot, StorageLocationTypeSchema as p, DEVICE_SETTINGS_CONTRIBUTION_METHODS as pt, looseSchema as q, ApiKeyRecordSchema as r, snapshotCapability as rt, CoreBlockSchema as s, userManagementCapability as st, ALL_CAPABILITY_DEFINITIONS as t, scoreRuntimes as tt, RUNTIME_DEFAULTS as u, errMsg as ut, alertsCapability as v, ReadinessRegistry as vt, coreBlockIdFromAddonId as w, createEvent as wt, bareAddonId as x, asJsonObject as xt, authProviderCapability as y, ReadinessTimeoutError as yt, isObjectInput as z };