@camstack/addon-pipeline 1.2.46 → 1.2.48

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-CPfFUIl9.js → dist-CksKE1Sn.js} +1399 -1285
  6. package/dist/{dist-CUj9mdhh.mjs → dist-DoTw0NM3.mjs} +1399 -1285
  7. package/dist/{event-loop-stall-monitor-md__iwWh.mjs → event-loop-stall-monitor-CtktCf3u.mjs} +1 -1
  8. package/dist/{event-loop-stall-monitor-BGbgcrEN.js → event-loop-stall-monitor-DzdwBr4X.js} +1 -1
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +3 -3
  12. package/dist/pipeline-runner/index.mjs +3 -3
  13. package/dist/recorder/index.js +1 -1
  14. package/dist/recorder/index.mjs +1 -1
  15. package/dist/session-decode/decode-worker-child.js +1 -1
  16. package/dist/session-decode/decode-worker-child.mjs +1 -1
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-aUWVCncO.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BNISghj9.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cj2xMupA.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Czrsozsg.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-CRwWRwJH.mjs → hostInit-DqD1qq8w.mjs} +2 -2
  22. package/dist/stream-broker/index.js +671 -101
  23. package/dist/stream-broker/index.mjs +671 -101
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/{worker-protocol-CD0M9YD7.mjs → worker-protocol-4_4L1QOe.mjs} +1 -1
  26. package/dist/{worker-protocol-hdTR-F5q.js → worker-protocol-CuEcTi66.js} +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C8eGXtf-.js → MaskShapeCanvas-DI4BY7W2-EcWZ5m8e.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-vvEcAgxS.js → MotionZonesSettings-NcxxQN8r-CfiyCD3z.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BLhx7VI-.js → PrivacyMaskSettings-APgPLF7p-ESPy4XXk.js} +1 -1
  30. package/embed-dist/assets/index-C3FKV1x-.js +112 -0
  31. package/embed-dist/index.html +1 -1
  32. package/package.json +1 -1
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DN2e2GJL.mjs +0 -26
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-2D_L2y3g.mjs +0 -26
  35. package/embed-dist/assets/index-DvOgpqcE.js +0 -112
@@ -7133,7 +7133,7 @@ function sleep(ms) {
7133
7133
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7134
7134
  }
7135
7135
  //#endregion
7136
- //#region ../types/dist/fmp4-box-splitter-B53u9-Nu.mjs
7136
+ //#region ../types/dist/canonical-hash-rO1sRmEK.mjs
7137
7137
  var AUDIO_ENCODER_BY_CODEC = {
7138
7138
  opus: "libopus",
7139
7139
  aac: "aac",
@@ -7538,37 +7538,6 @@ function invocationFromEncodeProfile(input) {
7538
7538
  ...input.audioSidecar !== void 0 ? { audioSidecar: input.audioSidecar } : {}
7539
7539
  };
7540
7540
  }
7541
- /**
7542
- * Deterministic SHA-256 hash of an arbitrary serialisable value. The
7543
- * canonical form sorts object keys alphabetically at every depth so two
7544
- * structurally-equal inputs with different key insertion orders produce
7545
- * the same hash. Returns a 64-char lowercase hex digest.
7546
- *
7547
- * Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
7548
- * accessory-rebuild work when the upstream shape is byte-identical to
7549
- * the last applied state — preventing user-visible "re-discovery"
7550
- * notifications on every addon-runner respawn. Each respawn re-fires
7551
- * `DeviceBindingsChanged` for every cap registration, which without
7552
- * this guard would propagate redundant pushes.
7553
- *
7554
- * Note: this is a SYMPTOMATIC fix layered on top of the binding-change
7555
- * subscription. The proper fix is a single "device ready" lifecycle
7556
- * barrier so exports react only when the full cap set has landed —
7557
- * tracked separately for post-HA-integration work.
7558
- */
7559
- function canonicalHash(value) {
7560
- const canonical = JSON.stringify(value, replaceWithSortedKeys);
7561
- return createHash("sha256").update(canonical ?? "").digest("hex");
7562
- }
7563
- function replaceWithSortedKeys(_key, value) {
7564
- if (value && typeof value === "object" && !Array.isArray(value)) {
7565
- const obj = value;
7566
- const out = {};
7567
- for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
7568
- return out;
7569
- }
7570
- return value;
7571
- }
7572
7541
  var DEFAULT_MAX_UNIT_BYTES = 16 * 1024 * 1024;
7573
7542
  /** Header size for a normal box, and for one carrying a 64-bit `largesize`. */
7574
7543
  var BOX_HEADER_BYTES = 8;
@@ -7741,6 +7710,37 @@ var Fmp4BoxSplitter = class {
7741
7710
  return [];
7742
7711
  }
7743
7712
  };
7713
+ /**
7714
+ * Deterministic SHA-256 hash of an arbitrary serialisable value. The
7715
+ * canonical form sorts object keys alphabetically at every depth so two
7716
+ * structurally-equal inputs with different key insertion orders produce
7717
+ * the same hash. Returns a 64-char lowercase hex digest.
7718
+ *
7719
+ * Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
7720
+ * accessory-rebuild work when the upstream shape is byte-identical to
7721
+ * the last applied state — preventing user-visible "re-discovery"
7722
+ * notifications on every addon-runner respawn. Each respawn re-fires
7723
+ * `DeviceBindingsChanged` for every cap registration, which without
7724
+ * this guard would propagate redundant pushes.
7725
+ *
7726
+ * Note: this is a SYMPTOMATIC fix layered on top of the binding-change
7727
+ * subscription. The proper fix is a single "device ready" lifecycle
7728
+ * barrier so exports react only when the full cap set has landed —
7729
+ * tracked separately for post-HA-integration work.
7730
+ */
7731
+ function canonicalHash(value) {
7732
+ const canonical = JSON.stringify(value, replaceWithSortedKeys);
7733
+ return createHash("sha256").update(canonical ?? "").digest("hex");
7734
+ }
7735
+ function replaceWithSortedKeys(_key, value) {
7736
+ if (value && typeof value === "object" && !Array.isArray(value)) {
7737
+ const obj = value;
7738
+ const out = {};
7739
+ for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
7740
+ return out;
7741
+ }
7742
+ return value;
7743
+ }
7744
7744
  //#endregion
7745
7745
  //#region ../types/dist/err-msg-IQTHeDzc.mjs
7746
7746
  /**
@@ -10017,6 +10017,69 @@ var StreamFormatSchema = _enum([
10017
10017
  "mjpeg",
10018
10018
  "rtsp"
10019
10019
  ]);
10020
+ /** A container `produceEventMedia` can emit. */
10021
+ var EventMediaKindSchema = _enum(["mp4", "gif"]);
10022
+ /**
10023
+ * One produced artifact, referenced by HANDLE.
10024
+ *
10025
+ * Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
10026
+ * method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
10027
+ * [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
10028
+ * on demand, compressed, by handle). `bytes` is here so a caller can decide
10029
+ * whether it wants the fetch at all.
10030
+ */
10031
+ var EventMediaArtifactSchema = object({
10032
+ kind: EventMediaKindSchema,
10033
+ /** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
10034
+ handle: string(),
10035
+ /**
10036
+ * The node holding the bytes — the ROUTING key for `fetchEventMedia`.
10037
+ *
10038
+ * `stream-broker` is a singleton cap and an unpinned call never leaves the
10039
+ * hub, so a handle produced on an agent's broker would be redeemed against
10040
+ * the hub's store and come back `null`. Same contract, same field name and
10041
+ * the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
10042
+ * lives and the consumer pins to it.
10043
+ */
10044
+ nodeId: string(),
10045
+ mime: string(),
10046
+ bytes: number().int(),
10047
+ width: number().int(),
10048
+ height: number().int()
10049
+ });
10050
+ /**
10051
+ * What a production actually covered — the answer to the only question an
10052
+ * operator asks about a notification clip.
10053
+ *
10054
+ * `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
10055
+ * so a caller can state "this clip starts 4.1 s before the event" instead of
10056
+ * inferring it from a duration. A production whose `fromTs` is later than the
10057
+ * event is a production with no pre-roll, and that is exactly the defect this
10058
+ * method exists to make visible rather than plausible.
10059
+ */
10060
+ var EventMediaCoverageSchema = object({
10061
+ fromTs: number(),
10062
+ toTs: number(),
10063
+ /** Encoded packets in the muxed window. */
10064
+ packets: number().int()
10065
+ });
10066
+ /**
10067
+ * The result of ONE cut, in every container the caller asked for.
10068
+ *
10069
+ * Every artifact in `media` came out of the SAME window of the SAME rendition —
10070
+ * that is the whole reason this is one method rather than one call per format.
10071
+ * A consumer attaching a gif and a video can no longer show two different
10072
+ * moments, because it never chose two sources.
10073
+ */
10074
+ var EventMediaProductionSchema = object({
10075
+ media: array(EventMediaArtifactSchema).readonly(),
10076
+ coverage: EventMediaCoverageSchema,
10077
+ /** The rendition actually cut from — what the default or the fallback chose. */
10078
+ profile: CamProfileSchema,
10079
+ /** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
10080
+ * source, a downscale, or a playback rate other than 1). */
10081
+ video: _enum(["copy", "encode"])
10082
+ });
10020
10083
  var RtspRestreamEntrySchema = object({
10021
10084
  brokerId: string(),
10022
10085
  url: string(),
@@ -10446,6 +10509,88 @@ var streamBrokerCapability = {
10446
10509
  kind: "mutation",
10447
10510
  auth: "admin"
10448
10511
  }),
10512
+ /**
10513
+ * THE production point for event media — one window, every container.
10514
+ *
10515
+ * `renderPreBufferClip` renders ONE format per call, so a consumer wanting a
10516
+ * gif and a video made two calls and got two windows. On 2026-08-08 the
10517
+ * operator received exactly that: a gif around a doorbell press and an mp4
10518
+ * of the six seconds after it. This method cuts once and derives the rest,
10519
+ * so "the attachments agree" is a property of the service instead of a
10520
+ * discipline every consumer has to keep.
10521
+ *
10522
+ * It is also the only place that owns the packets. The broker already
10523
+ * retains an AnnexB ring per camera (`setClipRetention`, D32) — the
10524
+ * prebuffer — so a production reaches BACKWARDS from the event with no
10525
+ * process to spawn and nothing kept warm. It waits out `postSeconds`, muxes
10526
+ * the window once (COPYING the camera's own H.264 whenever nothing is being
10527
+ * changed), derives any gif from that same mux, and returns HANDLES.
10528
+ *
10529
+ * Intended for every future consumer, not just notifications: a clips API, a
10530
+ * retrain export and an event export all want the same window of the same
10531
+ * camera and must not each grow their own renderer.
10532
+ */
10533
+ produceEventMedia: method(object({
10534
+ deviceId: number(),
10535
+ /** Absent = the largest H.264 rendition at or below 1080p, which is
10536
+ * also the one that can be copied. Falls back to whatever the ring
10537
+ * actually retained, and the answer says which. */
10538
+ profile: CamProfileSchema.optional(),
10539
+ aroundMs: number(),
10540
+ preSeconds: number().min(0).max(20).default(4),
10541
+ postSeconds: number().min(0).max(20).default(6),
10542
+ kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
10543
+ /** GIF geometry. The video keeps the source's own. */
10544
+ gifMaxWidth: number().int().min(120).max(1280).default(640),
10545
+ /**
10546
+ * The gif's own PLAYBACK rate in frames per second — what the finished
10547
+ * gif runs at, not how many source frames feed it. The decimation that
10548
+ * feeds it samples `gifFps / gifSpeed` source frames per second, so at
10549
+ * the defaults a 12 fps gif is built out of 3 source frames a second.
10550
+ */
10551
+ gifFps: number().int().min(1).max(15).default(12),
10552
+ /**
10553
+ * How fast the GIF plays against real time, independent of `speed`.
10554
+ *
10555
+ * 4× by default, by operator request: a notification gif is glanced at
10556
+ * on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
10557
+ * a separate knob from `speed` even though both now default to 4 —
10558
+ * a caller wanting a real-time video and a fast gif must not have to
10559
+ * choose.
10560
+ */
10561
+ gifSpeed: number().min(1).max(8).default(4),
10562
+ /**
10563
+ * Playback rate of the VIDEO. Also 4× by default, by operator decision.
10564
+ *
10565
+ * `1` is real time and is the ONLY value that allows the copy branch —
10566
+ * anything else forces `libx264` over the window. That was priced
10567
+ * before it was chosen: a per-event burst measured at 0.23 s and 254 KB
10568
+ * on a real 615 720p cut, against 922 KB for the copy it replaces. A
10569
+ * re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
10570
+ * once the decode is forced the width stops being free.
10571
+ */
10572
+ speed: number().min(1).max(8).default(4)
10573
+ }), EventMediaProductionSchema, {
10574
+ kind: "mutation",
10575
+ auth: "admin"
10576
+ }),
10577
+ /**
10578
+ * Redeem a {@link EventMediaArtifactSchema} handle for its bytes.
10579
+ *
10580
+ * Separate from the production on purpose (D18): the producing runner keeps
10581
+ * the artifact for a few minutes, and a consumer that only wanted the
10582
+ * coverage never moves a megabyte. `null` once it has expired — a handle is
10583
+ * short-lived by design and a caller that waited too long must see that
10584
+ * rather than a zero-length file.
10585
+ */
10586
+ fetchEventMedia: method(object({ handle: string() }), object({
10587
+ base64: string(),
10588
+ mime: string(),
10589
+ bytes: number().int()
10590
+ }).nullable(), {
10591
+ kind: "mutation",
10592
+ auth: "admin"
10593
+ }),
10449
10594
  listAllCameraStreams: method(_void(), array(CameraStreamSchema).readonly()),
10450
10595
  listAllProfileSlots: method(_void(), array(ProfileSlotSchema).readonly()),
10451
10596
  getBrokerStats: method(object({ brokerId: string() }), BrokerStatsSchema),
@@ -10875,25 +11020,6 @@ var cameraStreamsCapability = {
10875
11020
  lastChangedAt: number()
10876
11021
  })
10877
11022
  };
10878
- /**
10879
- * core-blocks — user-authored TypeScript, stored in the kernel and executed in
10880
- * its own process.
10881
- *
10882
- * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
10883
- *
10884
- * The first use is **owning devices without being a device provider**: a block
10885
- * declares devices under a system or custom integration and drives their state,
10886
- * with the same `ctx` an addon gets. Automations come later; nothing here
10887
- * models a trigger.
10888
- *
10889
- * **Stated plainly, because it does not change by being true:** a block has an
10890
- * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
10891
- * with no review step. What makes that survivable is not a sandbox, it is
10892
- * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
10893
- * so a block that throws or never returns is marked `failed` and visible
10894
- * instead of taking the hub with it (D6). Every method here is admin-only, and
10895
- * must stay so.
10896
- */
10897
11023
  /** Where a block runs. The operator chooses — a block driving a device on an
10898
11024
  * agent is the reason placement is not fixed to the hub. */
10899
11025
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
@@ -10965,6 +11091,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
10965
11091
  }), object({ block: CoreBlockSchema }), {
10966
11092
  kind: "mutation",
10967
11093
  auth: "admin"
11094
+ }), method(object({ blockId: string() }), object({ block: CoreBlockSchema }), {
11095
+ kind: "mutation",
11096
+ auth: "admin"
10968
11097
  }), method(object({ code: string() }), CoreBlockCompileResultSchema, {
10969
11098
  kind: "mutation",
10970
11099
  auth: "admin"
@@ -11696,713 +11825,118 @@ var ExposeInputSchema = object({
11696
11825
  });
11697
11826
  var UnexposeInputSchema = object({ deviceId: string() });
11698
11827
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), { kind: "mutation" }), method(UnexposeInputSchema, _void(), { kind: "mutation" });
11828
+ var ProviderStatusSchema = object({
11829
+ connected: boolean(),
11830
+ deviceCount: number(),
11831
+ error: string().optional()
11832
+ });
11833
+ object({
11834
+ externalId: string(),
11835
+ name: string(),
11836
+ type: string(),
11837
+ metadata: record(string(), unknown()).optional()
11838
+ });
11699
11839
  /**
11700
- * Resource-bound constants for the safe expression engine.
11701
- *
11702
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
11703
- * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
11704
- * O(nodeCount) by construction. These caps merely put a hard ceiling on the
11705
- * work a single author-supplied expression can request, so a hostile or
11706
- * accidental pathological string can never spend unbounded CPU/memory.
11840
+ * Candidate handed back from discovery and accepted by
11841
+ * `adoptDiscoveredDevice`. Shape mirrors the in-process
11842
+ * `DiscoveredDevice` interface used by `DeviceDiscovery`.
11707
11843
  */
11708
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
11709
- * rejected without allocation. */
11710
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
11711
- /** A legal binding / identifier name. */
11712
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
11713
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
11714
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
11715
- var RESERVED_BINDING_NAMES = new Set([
11716
- "now",
11717
- "true",
11718
- "false",
11719
- "null"
11720
- ]);
11844
+ var DiscoveryCandidateSchema = object({
11845
+ stableId: string(),
11846
+ type: _enum(DeviceType),
11847
+ suggestedName: string(),
11848
+ prefilledConfig: record(string(), unknown()),
11849
+ /**
11850
+ * Optional upstream-system identity (HA entity_id, vendor MAC, …).
11851
+ * Discovery pre-populates this for systems that know the upstream
11852
+ * identity ahead of adoption. Rendering metadata (unit, precision)
11853
+ * flows live through the cap STATUS SLICE after adoption.
11854
+ */
11855
+ sourceInfo: SourceInfoSchema.optional()
11856
+ });
11721
11857
  /**
11722
- * Error types for the safe expression engine. Two distinct classes so callers
11723
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
11724
- * failure both are non-fatal to the host: read paths degrade to "skip link".
11858
+ * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
11859
+ * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
11860
+ * tRPC layer can pass it through without reshaping.
11725
11861
  */
11726
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
11727
- * the failure is anchored to a character (author-facing inline feedback). */
11728
- var ExpressionParseError = class extends Error {
11729
- position;
11730
- constructor(message, position) {
11731
- super(message);
11732
- this.name = "ExpressionParseError";
11733
- this.position = position;
11734
- }
11735
- };
11736
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
11737
- * result, unknown builtin, step-budget exceeded). */
11738
- var ExpressionEvalError = class extends Error {
11739
- constructor(message) {
11740
- super(message);
11741
- this.name = "ExpressionEvalError";
11742
- }
11743
- };
11862
+ var DeviceSummarySchema = object({
11863
+ id: number(),
11864
+ stableId: string(),
11865
+ addonId: string(),
11866
+ type: string(),
11867
+ name: string(),
11868
+ parentDeviceId: number().nullable(),
11869
+ online: boolean(),
11870
+ features: array(string()),
11871
+ config: record(string(), unknown()),
11872
+ /** Optional upstream-system identity (dispatch key + system tag).
11873
+ * See `SourceInfo`. Present when the device has a non-synthetic
11874
+ * source identifier (HA entities, vendor MAC, …); omitted when the
11875
+ * synthetic backfill is in effect. */
11876
+ sourceInfo: SourceInfoSchema.optional()
11877
+ });
11744
11878
  /**
11745
- * Frozen, null-prototype builtin function table for the expression engine
11746
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
11747
- * parser rejects any callee not in it, and the evaluator gates each call on an
11748
- * own-property check against it.
11749
- *
11750
- * Because the object has a NULL prototype AND is `Object.freeze`d:
11751
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
11752
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
11753
- * (there is no `Object.prototype` in the chain), so those names are not
11754
- * callable — they are simply "unknown function" at parse time.
11755
- *
11756
- * Every numeric argument is validated as a finite number and every numeric
11757
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
11758
- * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
11759
- * closed rather than emitting a garbage value.
11879
+ * Result of a live field test (e.g. probing an RTSP URL during device
11880
+ * creation). Matches the UI-side `FieldProbeResult` in
11881
+ * `interfaces/config-ui.ts` the admin `FormBuilder` renders the
11882
+ * returned `labels` as chips next to the input.
11760
11883
  */
11761
- function asFiniteNumber(value, name, index) {
11762
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
11763
- return value;
11764
- }
11765
- function asString$1(value, name, index) {
11766
- if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
11767
- return value;
11768
- }
11769
- function finiteResult(value, name) {
11770
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
11771
- return value;
11772
- }
11773
- function allFiniteNumbers(args, name) {
11774
- return args.map((a, idx) => asFiniteNumber(a, name, idx));
11775
- }
11776
- var INF = Number.POSITIVE_INFINITY;
11777
- var table = {
11778
- min: {
11779
- minArgs: 1,
11780
- maxArgs: INF,
11781
- apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
11782
- },
11783
- max: {
11784
- minArgs: 1,
11785
- maxArgs: INF,
11786
- apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
11787
- },
11788
- abs: {
11789
- minArgs: 1,
11790
- maxArgs: 1,
11791
- apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
11792
- },
11793
- floor: {
11794
- minArgs: 1,
11795
- maxArgs: 1,
11796
- apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
11797
- },
11798
- ceil: {
11799
- minArgs: 1,
11800
- maxArgs: 1,
11801
- apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
11802
- },
11803
- sqrt: {
11804
- minArgs: 1,
11805
- maxArgs: 1,
11806
- apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
11807
- },
11808
- round: {
11809
- minArgs: 1,
11810
- maxArgs: 2,
11811
- apply: (args) => {
11812
- const x = asFiniteNumber(args[0], "round", 0);
11813
- const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
11814
- if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
11815
- const factor = 10 ** digits;
11816
- return finiteResult(Math.round(x * factor) / factor, "round");
11817
- }
11818
- },
11819
- pow: {
11820
- minArgs: 2,
11821
- maxArgs: 2,
11822
- apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
11823
- },
11824
- clamp: {
11825
- minArgs: 3,
11826
- maxArgs: 3,
11827
- apply: (args) => {
11828
- const x = asFiniteNumber(args[0], "clamp", 0);
11829
- const lo = asFiniteNumber(args[1], "clamp", 1);
11830
- const hi = asFiniteNumber(args[2], "clamp", 2);
11831
- if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
11832
- return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
11833
- }
11834
- },
11835
- avg: {
11836
- minArgs: 1,
11837
- maxArgs: INF,
11838
- apply: (args) => {
11839
- const nums = allFiniteNumbers(args, "avg");
11840
- return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
11841
- }
11842
- },
11843
- sum: {
11844
- minArgs: 1,
11845
- maxArgs: INF,
11846
- apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
11847
- },
11848
- coalesce: {
11849
- minArgs: 1,
11850
- maxArgs: INF,
11851
- apply: (args) => {
11852
- for (const a of args) if (a !== null) return a;
11853
- return null;
11854
- }
11855
- },
11856
- age: {
11857
- minArgs: 2,
11858
- maxArgs: 2,
11859
- apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
11860
- },
11861
- convert: {
11862
- minArgs: 3,
11863
- maxArgs: 3,
11864
- apply: (args, hooks) => {
11865
- const x = asFiniteNumber(args[0], "convert", 0);
11866
- const from = asString$1(args[1], "convert", 1).trim();
11867
- const to = asString$1(args[2], "convert", 2).trim();
11868
- if (hooks.convert) {
11869
- const out = hooks.convert(x, from, to);
11870
- if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
11871
- return finiteResult(out, "convert");
11872
- }
11873
- if (from === to) return x;
11874
- throw new ExpressionEvalError("convert: unit conversion table not installed");
11875
- }
11876
- }
11877
- };
11878
- Object.freeze(Object.assign(Object.create(null), table));
11879
- /** The set of valid builtin names — used by the parser to reject unknown
11880
- * callees at parse time (immediate author feedback). */
11881
- var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
11882
- /**
11883
- * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
11884
- * zero-dependency. The grammar is deliberately boring: decimal numbers,
11885
- * single/double-quoted strings with a tiny escape set, identifiers, the three
11886
- * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
11887
- * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
11888
- * is a parse error with a source position, so member access / assignment /
11889
- * template literals are lexically impossible.
11890
- */
11891
- var KEYWORDS = new Set([
11892
- "true",
11893
- "false",
11894
- "null"
11895
- ]);
11896
- function isDigit(ch) {
11897
- return ch >= "0" && ch <= "9";
11898
- }
11899
- function isIdentStart(ch) {
11900
- return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
11901
- }
11902
- function isIdentPart(ch) {
11903
- return isIdentStart(ch) || isDigit(ch);
11904
- }
11905
- function isWhitespace(ch) {
11906
- return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
11907
- }
11908
- /** Tokenize `source` into a flat token list ending with a single `eof` token.
11909
- * Throws `ExpressionParseError` on any illegal character or unterminated
11910
- * string. */
11911
- function tokenize(source) {
11912
- if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
11913
- const tokens = [];
11914
- let i = 0;
11915
- const n = source.length;
11916
- while (i < n) {
11917
- const ch = source[i];
11918
- if (isWhitespace(ch)) {
11919
- i += 1;
11920
- continue;
11921
- }
11922
- if (isDigit(ch)) {
11923
- const start = i;
11924
- while (i < n && isDigit(source[i])) i += 1;
11925
- if (i < n && source[i] === ".") {
11926
- if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
11927
- i += 1;
11928
- while (i < n && isDigit(source[i])) i += 1;
11929
- }
11930
- const text = source.slice(start, i);
11931
- const value = Number(text);
11932
- if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
11933
- tokens.push({
11934
- type: "number",
11935
- value,
11936
- pos: start
11937
- });
11938
- continue;
11939
- }
11940
- if (ch === "'" || ch === "\"") {
11941
- const quote = ch;
11942
- const start = i;
11943
- i += 1;
11944
- let out = "";
11945
- let closed = false;
11946
- while (i < n) {
11947
- const c = source[i];
11948
- if (c === "\\") {
11949
- const next = i + 1 < n ? source[i + 1] : "";
11950
- if (next === "\\" || next === "'" || next === "\"") {
11951
- out += next;
11952
- i += 2;
11953
- continue;
11954
- }
11955
- throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
11956
- }
11957
- if (c === quote) {
11958
- closed = true;
11959
- i += 1;
11960
- break;
11961
- }
11962
- out += c;
11963
- i += 1;
11964
- }
11965
- if (!closed) throw new ExpressionParseError("unterminated string literal", start);
11966
- tokens.push({
11967
- type: "string",
11968
- value: out,
11969
- pos: start
11970
- });
11971
- continue;
11972
- }
11973
- if (isIdentStart(ch)) {
11974
- const start = i;
11975
- while (i < n && isIdentPart(source[i])) i += 1;
11976
- const text = source.slice(start, i);
11977
- if (KEYWORDS.has(text)) tokens.push({
11978
- type: "keyword",
11979
- keyword: keywordOf(text),
11980
- pos: start
11981
- });
11982
- else tokens.push({
11983
- type: "identifier",
11984
- name: text,
11985
- pos: start
11986
- });
11987
- continue;
11988
- }
11989
- const two = i + 1 < n ? source.slice(i, i + 2) : "";
11990
- if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
11991
- tokens.push({
11992
- type: "punct",
11993
- punct: two,
11994
- pos: i
11995
- });
11996
- i += 2;
11997
- continue;
11998
- }
11999
- if (isSinglePunct(ch)) {
12000
- tokens.push({
12001
- type: "punct",
12002
- punct: ch,
12003
- pos: i
12004
- });
12005
- i += 1;
12006
- continue;
12007
- }
12008
- throw new ExpressionParseError(`unexpected character '${ch}'`, i);
12009
- }
12010
- tokens.push({
12011
- type: "eof",
12012
- pos: n
12013
- });
12014
- return tokens;
12015
- }
12016
- function keywordOf(text) {
12017
- if (text === "true") return "true";
12018
- if (text === "false") return "false";
12019
- return "null";
12020
- }
12021
- function isSinglePunct(ch) {
12022
- return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
12023
- }
11884
+ var FieldProbeResultSchema = object({
11885
+ status: _enum(["ok", "error"]),
11886
+ labels: array(string()).optional(),
11887
+ error: string().optional()
11888
+ });
12024
11889
  /**
12025
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
12026
- *
12027
- * Precedence (low high): ternary `?:` (right-assoc) `||` `&&` → equality
12028
- * relational additive → multiplicative → unary `! -` → call / primary.
12029
- * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
12030
- * string validated against the builtin table at parse time, so an unknown
12031
- * function is rejected immediately (author feedback) and a persisted expression
12032
- * that references a since-removed builtin degrades at read.
12033
- *
12034
- * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
12035
- * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
11890
+ * The output of `getChildCreationSchema` is a UI schema tree. We store
11891
+ * it as `unknown` at the capability layer — the router just passes it
11892
+ * through and the admin UI renders it via `FormBuilder`. The actual
11893
+ * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
11894
+ * but we deliberately avoid a Zod mirror because the union is large and
11895
+ * not meant for runtime validation at this seam.
12036
11896
  */
12037
- /** Binary/logical operator precedence (higher binds tighter). */
12038
- var BINARY_PRECEDENCE = {
12039
- "||": 1,
12040
- "&&": 2,
12041
- "==": 3,
12042
- "!=": 3,
12043
- "<": 4,
12044
- "<=": 4,
12045
- ">": 4,
12046
- ">=": 4,
12047
- "+": 5,
12048
- "-": 5,
12049
- "*": 6,
12050
- "/": 6,
12051
- "%": 6
12052
- };
12053
- function isLogicalOp(op) {
12054
- return op === "&&" || op === "||";
12055
- }
12056
- function isBinaryOp(op) {
12057
- return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
12058
- }
12059
- var Parser = class {
12060
- tokens;
12061
- pos = 0;
12062
- nodeCount = 0;
12063
- identifiers = /* @__PURE__ */ new Set();
12064
- callees = /* @__PURE__ */ new Set();
12065
- constructor(tokens) {
12066
- this.tokens = tokens;
12067
- }
12068
- parse() {
12069
- const ast = this.parseTernary();
12070
- const tok = this.peek();
12071
- if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
12072
- return {
12073
- ast,
12074
- identifiers: this.identifiers,
12075
- callees: this.callees,
12076
- nodeCount: this.nodeCount
12077
- };
12078
- }
12079
- peek() {
12080
- return this.tokens[this.pos];
12081
- }
12082
- next() {
12083
- return this.tokens[this.pos++];
12084
- }
12085
- /** Consume a punctuator token, erroring if the next token isn't it. */
12086
- expectPunct(punct) {
12087
- const tok = this.peek();
12088
- if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
12089
- this.pos += 1;
12090
- }
12091
- matchPunct(punct) {
12092
- const tok = this.peek();
12093
- if (tok.type === "punct" && tok.punct === punct) {
12094
- this.pos += 1;
12095
- return true;
12096
- }
12097
- return false;
12098
- }
12099
- countNode() {
12100
- this.nodeCount += 1;
12101
- if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
12102
- }
12103
- parseTernary() {
12104
- const test = this.parseBinary(1);
12105
- if (this.matchPunct("?")) {
12106
- const consequent = this.parseTernary();
12107
- this.expectPunct(":");
12108
- const alternate = this.parseTernary();
12109
- this.countNode();
12110
- return {
12111
- kind: "conditional",
12112
- test,
12113
- consequent,
12114
- alternate
12115
- };
12116
- }
12117
- return test;
12118
- }
12119
- parseBinary(minPrec) {
12120
- let left = this.parseUnary();
12121
- for (;;) {
12122
- const tok = this.peek();
12123
- if (tok.type !== "punct") break;
12124
- const prec = BINARY_PRECEDENCE[tok.punct];
12125
- if (prec === void 0 || prec < minPrec) break;
12126
- const op = tok.punct;
12127
- this.pos += 1;
12128
- const right = this.parseBinary(prec + 1);
12129
- this.countNode();
12130
- if (isLogicalOp(op)) left = {
12131
- kind: "logical",
12132
- op,
12133
- left,
12134
- right
12135
- };
12136
- else if (isBinaryOp(op)) left = {
12137
- kind: "binary",
12138
- op,
12139
- left,
12140
- right
12141
- };
12142
- else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
12143
- }
12144
- return left;
12145
- }
12146
- parseUnary() {
12147
- const tok = this.peek();
12148
- if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
12149
- const op = tok.punct;
12150
- this.pos += 1;
12151
- const operand = this.parseUnary();
12152
- this.countNode();
12153
- return {
12154
- kind: "unary",
12155
- op,
12156
- operand
12157
- };
12158
- }
12159
- return this.parsePrimary();
12160
- }
12161
- parsePrimary() {
12162
- const tok = this.next();
12163
- switch (tok.type) {
12164
- case "number":
12165
- this.countNode();
12166
- return {
12167
- kind: "literal",
12168
- value: tok.value
12169
- };
12170
- case "string":
12171
- this.countNode();
12172
- return {
12173
- kind: "literal",
12174
- value: tok.value
12175
- };
12176
- case "keyword":
12177
- this.countNode();
12178
- return {
12179
- kind: "literal",
12180
- value: tok.keyword === "null" ? null : tok.keyword === "true"
12181
- };
12182
- case "identifier": {
12183
- const nextTok = this.peek();
12184
- if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
12185
- this.identifiers.add(tok.name);
12186
- this.countNode();
12187
- return {
12188
- kind: "identifier",
12189
- name: tok.name
12190
- };
12191
- }
12192
- case "punct":
12193
- if (tok.punct === "(") {
12194
- const inner = this.parseTernary();
12195
- this.expectPunct(")");
12196
- return inner;
12197
- }
12198
- throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
12199
- case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
12200
- }
12201
- }
12202
- parseCall(callee, pos) {
12203
- if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
12204
- this.expectPunct("(");
12205
- const args = [];
12206
- if (!this.matchPunct(")")) for (;;) {
12207
- args.push(this.parseTernary());
12208
- if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
12209
- if (this.matchPunct(",")) continue;
12210
- this.expectPunct(")");
12211
- break;
12212
- }
12213
- this.callees.add(callee);
12214
- this.countNode();
12215
- return {
12216
- kind: "call",
12217
- callee,
12218
- args
12219
- };
12220
- }
12221
- };
12222
- /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
12223
- * `ExpressionParseError` on any lexical or grammatical failure. */
12224
- function parseExpression(source) {
12225
- return new Parser(tokenize(source)).parse();
12226
- }
11897
+ var CreationSchemaOutputSchema = unknown();
11898
+ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
11899
+ id: string(),
11900
+ name: string(),
11901
+ type: string()
11902
+ }))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
11903
+ kind: "mutation",
11904
+ auth: "admin"
11905
+ }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
11906
+ kind: "mutation",
11907
+ auth: "admin"
11908
+ }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
11909
+ type: _enum(DeviceType),
11910
+ config: record(string(), unknown())
11911
+ }), DeviceSummarySchema, {
11912
+ kind: "mutation",
11913
+ auth: "admin"
11914
+ }), method(object({
11915
+ type: _enum(DeviceType),
11916
+ key: string(),
11917
+ value: unknown(),
11918
+ formValues: record(string(), unknown()).optional()
11919
+ }), FieldProbeResultSchema, {
11920
+ kind: "mutation",
11921
+ auth: "admin"
11922
+ });
12227
11923
  /**
12228
- * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
12229
- * by expr"). The cache stores BOTH successes and failures (negative caching),
12230
- * so a corrupt persisted string costs exactly one tokenize+parse total — not
12231
- * one per read on a hot resolve path.
11924
+ * Device Manager capability hub-side singleton that unifies device persistence,
11925
+ * live registry access, and all management operations into a single tRPC surface.
12232
11926
  *
12233
- * The cache is a module-level singleton: entries are pure, content-addressed
12234
- * ASTs keyed by the raw source string, so sharing one instance across all
12235
- * callers is safe and maximises hit rate.
12236
- */
12237
- var cache = /* @__PURE__ */ new Map();
12238
- function getCached(source) {
12239
- const hit = cache.get(source);
12240
- if (hit !== void 0) {
12241
- cache.delete(source);
12242
- cache.set(source, hit);
12243
- return hit;
12244
- }
12245
- let result;
12246
- try {
12247
- result = {
12248
- ok: true,
12249
- parsed: parseExpression(source)
12250
- };
12251
- } catch (err) {
12252
- result = {
12253
- ok: false,
12254
- error: err instanceof ExpressionParseError ? err.message : String(err)
12255
- };
12256
- }
12257
- cache.set(source, result);
12258
- if (cache.size > 256) {
12259
- const oldest = cache.keys().next().value;
12260
- if (oldest !== void 0) cache.delete(oldest);
12261
- }
12262
- return result;
12263
- }
12264
- /** Compile `source`, returning a discriminated result instead of throwing.
12265
- * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
12266
- function compileExpressionSafe(source) {
12267
- return getCached(source);
12268
- }
12269
- Object.freeze({});
12270
- /**
12271
- * Author-time validation. Returns `null` when the source is valid, else a
12272
- * human-readable error message. Checks: the expression compiles; binding count
12273
- * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
12274
- * is not reserved (`now`/keywords) and does not shadow a builtin; and every
12275
- * FREE identifier of the AST is covered by a binding or the injected `now`.
12276
- */
12277
- function validateExpressionSource(src) {
12278
- const names = Object.keys(src.bindings);
12279
- if (names.length > 32) return `too many bindings (${names.length} > 32)`;
12280
- for (const name of names) {
12281
- if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
12282
- if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
12283
- if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
12284
- }
12285
- const compiled = compileExpressionSafe(src.expr);
12286
- if (!compiled.ok) return compiled.error;
12287
- const bound = new Set(names);
12288
- for (const id of compiled.parsed.identifiers) {
12289
- if (id === "now") continue;
12290
- if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
12291
- }
12292
- return null;
12293
- }
12294
- var ProviderStatusSchema = object({
12295
- connected: boolean(),
12296
- deviceCount: number(),
12297
- error: string().optional()
12298
- });
12299
- object({
12300
- externalId: string(),
12301
- name: string(),
12302
- type: string(),
12303
- metadata: record(string(), unknown()).optional()
12304
- });
12305
- /**
12306
- * Candidate handed back from discovery and accepted by
12307
- * `adoptDiscoveredDevice`. Shape mirrors the in-process
12308
- * `DiscoveredDevice` interface used by `DeviceDiscovery`.
12309
- */
12310
- var DiscoveryCandidateSchema = object({
12311
- stableId: string(),
12312
- type: _enum(DeviceType),
12313
- suggestedName: string(),
12314
- prefilledConfig: record(string(), unknown()),
12315
- /**
12316
- * Optional upstream-system identity (HA entity_id, vendor MAC, …).
12317
- * Discovery pre-populates this for systems that know the upstream
12318
- * identity ahead of adoption. Rendering metadata (unit, precision)
12319
- * flows live through the cap STATUS SLICE after adoption.
12320
- */
12321
- sourceInfo: SourceInfoSchema.optional()
12322
- });
12323
- /**
12324
- * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
12325
- * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
12326
- * tRPC layer can pass it through without reshaping.
12327
- */
12328
- var DeviceSummarySchema = object({
12329
- id: number(),
12330
- stableId: string(),
12331
- addonId: string(),
12332
- type: string(),
12333
- name: string(),
12334
- parentDeviceId: number().nullable(),
12335
- online: boolean(),
12336
- features: array(string()),
12337
- config: record(string(), unknown()),
12338
- /** Optional upstream-system identity (dispatch key + system tag).
12339
- * See `SourceInfo`. Present when the device has a non-synthetic
12340
- * source identifier (HA entities, vendor MAC, …); omitted when the
12341
- * synthetic backfill is in effect. */
12342
- sourceInfo: SourceInfoSchema.optional()
12343
- });
12344
- /**
12345
- * Result of a live field test (e.g. probing an RTSP URL during device
12346
- * creation). Matches the UI-side `FieldProbeResult` in
12347
- * `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
12348
- * returned `labels` as chips next to the input.
12349
- */
12350
- var FieldProbeResultSchema = object({
12351
- status: _enum(["ok", "error"]),
12352
- labels: array(string()).optional(),
12353
- error: string().optional()
12354
- });
12355
- /**
12356
- * The output of `getChildCreationSchema` is a UI schema tree. We store
12357
- * it as `unknown` at the capability layer — the router just passes it
12358
- * through and the admin UI renders it via `FormBuilder`. The actual
12359
- * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
12360
- * but we deliberately avoid a Zod mirror because the union is large and
12361
- * not meant for runtime validation at this seam.
12362
- */
12363
- var CreationSchemaOutputSchema = unknown();
12364
- method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
12365
- id: string(),
12366
- name: string(),
12367
- type: string()
12368
- }))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
12369
- kind: "mutation",
12370
- auth: "admin"
12371
- }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
12372
- kind: "mutation",
12373
- auth: "admin"
12374
- }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
12375
- type: _enum(DeviceType),
12376
- config: record(string(), unknown())
12377
- }), DeviceSummarySchema, {
12378
- kind: "mutation",
12379
- auth: "admin"
12380
- }), method(object({
12381
- type: _enum(DeviceType),
12382
- key: string(),
12383
- value: unknown(),
12384
- formValues: record(string(), unknown()).optional()
12385
- }), FieldProbeResultSchema, {
12386
- kind: "mutation",
12387
- auth: "admin"
12388
- });
12389
- /**
12390
- * Device Manager capability — hub-side singleton that unifies device persistence,
12391
- * live registry access, and all management operations into a single tRPC surface.
12392
- *
12393
- * Replaces:
12394
- * - `device-persistence` capability (persistence methods absorbed here)
12395
- * - `device-management.router.ts` (deleted in Phase 2)
12396
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
12397
- *
12398
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
12399
- * fork into separate processes but never run on remote cluster agents. Therefore:
12400
- * - No nodeId routing needed — this is a pure hub singleton.
12401
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
12402
- * - No shadow registry or cross-node aggregation required.
12403
- *
12404
- * Forked workers register devices back to the hub via `ctx.devices`
12405
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
11927
+ * Replaces:
11928
+ * - `device-persistence` capability (persistence methods absorbed here)
11929
+ * - `device-management.router.ts` (deleted in Phase 2)
11930
+ * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
11931
+ *
11932
+ * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
11933
+ * fork into separate processes but never run on remote cluster agents. Therefore:
11934
+ * - No nodeId routing needed — this is a pure hub singleton.
11935
+ * - The hub's DeviceRegistry is the single source of truth for all live devices.
11936
+ * - No shadow registry or cross-node aggregation required.
11937
+ *
11938
+ * Forked workers register devices back to the hub via `ctx.devices`
11939
+ * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
12406
11940
  */
12407
11941
  /** One child-placement directive on a container's `childLayout`. Structurally
12408
11942
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
@@ -12415,92 +11949,6 @@ var ChildLayoutEntrySchema = object({
12415
11949
  order: number().optional(),
12416
11950
  collapsed: boolean().optional()
12417
11951
  });
12418
- /** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
12419
- * `device-management.ts`. Source is a union: a FIELD source copies a sibling
12420
- * accessory's status field (`kind` optional/absent for wire compat); a
12421
- * LITERAL source carries a per-device constant (no sibling is read); a
12422
- * GLOBAL source (P2e) copies ANY device's status field, addressed by the
12423
- * source device's full re-sync-stable `stableId`. */
12424
- var DeviceLinkFieldSourceSchema = object({
12425
- kind: literal("field").optional(),
12426
- sourceKey: string(),
12427
- cap: string(),
12428
- fieldPath: string()
12429
- });
12430
- var DeviceLinkLiteralSourceSchema = object({
12431
- kind: literal("literal"),
12432
- value: union([
12433
- string(),
12434
- number(),
12435
- boolean(),
12436
- _null()
12437
- ])
12438
- });
12439
- var DeviceLinkGlobalSourceSchema = object({
12440
- kind: literal("global"),
12441
- sourceStableId: string(),
12442
- cap: string(),
12443
- fieldPath: string()
12444
- });
12445
- /** Expression source (Stage X): compute the target field from N named bindings
12446
- * via the safe expression engine. Bindings are field | literal | global — never
12447
- * another expression (no nesting). The `superRefine` runs the SAME author-time
12448
- * validation as `validateExpressionSource` (compiles the expr, checks binding
12449
- * names + identifier coverage) so every wire boundary that parses a DeviceLink
12450
- * (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
12451
- * Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
12452
- var DeviceLinkExpressionSourceSchema = object({
12453
- kind: literal("expression"),
12454
- expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
12455
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
12456
- DeviceLinkFieldSourceSchema,
12457
- DeviceLinkLiteralSourceSchema,
12458
- DeviceLinkGlobalSourceSchema
12459
- ]))
12460
- }).superRefine((src, ctx) => {
12461
- const err = validateExpressionSource(src);
12462
- if (err !== null) ctx.addIssue({
12463
- code: "custom",
12464
- message: err,
12465
- path: ["expr"]
12466
- });
12467
- });
12468
- var DeviceLinkSchema = object({
12469
- id: string(),
12470
- source: union([
12471
- DeviceLinkFieldSourceSchema,
12472
- DeviceLinkLiteralSourceSchema,
12473
- DeviceLinkGlobalSourceSchema,
12474
- DeviceLinkExpressionSourceSchema
12475
- ]),
12476
- target: object({
12477
- cap: string(),
12478
- fieldPath: string(),
12479
- itemKey: string().optional()
12480
- }),
12481
- transform: discriminatedUnion("kind", [
12482
- object({ kind: literal("identity") }),
12483
- object({
12484
- kind: literal("enum-map"),
12485
- mapping: record(string(), union([
12486
- string(),
12487
- number(),
12488
- boolean()
12489
- ])),
12490
- fallback: union([
12491
- string(),
12492
- number(),
12493
- boolean()
12494
- ]).optional()
12495
- }),
12496
- object({
12497
- kind: literal("linear"),
12498
- scale: number(),
12499
- offset: number(),
12500
- clamp: tuple([number(), number()]).readonly().optional()
12501
- })
12502
- ]).optional()
12503
- });
12504
11952
  /** Cap-wire shape of a per-cap display refinement — mirrors
12505
11953
  * `DeviceCapDisplayOverride` in `device-management.ts`. */
12506
11954
  var DeviceCapDisplayOverrideSchema = object({
@@ -12580,8 +12028,6 @@ var DeviceInfoSchema = object({
12580
12028
  * named accordion sections (with optional intra-section order). See
12581
12029
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
12582
12030
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
12583
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
12584
- deviceLinks: array(DeviceLinkSchema).readonly().optional(),
12585
12031
  /** Operator-authored per-device display override. See `DeviceMeta.display`. */
12586
12032
  display: DeviceDisplayOverrideSchema.optional()
12587
12033
  });
@@ -12590,7 +12036,7 @@ var ConfigEntrySchema = object({
12590
12036
  value: unknown(),
12591
12037
  description: string().optional()
12592
12038
  });
12593
- var DeviceLinkModeSchema = _enum(["auto", "manual"]);
12039
+ var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
12594
12040
  /** One resolved linked device — the compact projection consumers need. */
12595
12041
  var LinkedDeviceSchema = object({
12596
12042
  deviceId: number(),
@@ -12653,8 +12099,6 @@ var DeviceMetaSchema = object({
12653
12099
  * accordion sections (with optional intra-section order). See
12654
12100
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
12655
12101
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
12656
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
12657
- deviceLinks: array(DeviceLinkSchema).readonly().optional(),
12658
12102
  /** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
12659
12103
  * Optional: only present for accessory children that carry a known role. */
12660
12104
  role: string().nullable().optional(),
@@ -12747,12 +12191,6 @@ method(object({
12747
12191
  }), _void(), {
12748
12192
  kind: "mutation",
12749
12193
  auth: "admin"
12750
- }), method(object({
12751
- deviceId: number(),
12752
- deviceLinks: array(DeviceLinkSchema).readonly()
12753
- }), _void(), {
12754
- kind: "mutation",
12755
- auth: "admin"
12756
12194
  }), method(object({
12757
12195
  deviceId: number(),
12758
12196
  display: DeviceDisplayOverrideSchema.nullable()
@@ -12834,7 +12272,7 @@ method(object({
12834
12272
  * shipping 293 rows to find 12. */
12835
12273
  isCamera: boolean().optional()
12836
12274
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12837
- mode: DeviceLinkModeSchema,
12275
+ mode: LinkedDevicesModeSchema,
12838
12276
  devices: array(LinkedDeviceSchema)
12839
12277
  })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12840
12278
  deviceId: number(),
@@ -12867,11 +12305,7 @@ method(object({
12867
12305
  deviceId: number(),
12868
12306
  entries: array(object({
12869
12307
  capName: string(),
12870
- kind: _enum([
12871
- "native",
12872
- "wrapped",
12873
- "linked"
12874
- ]),
12308
+ kind: _enum(["native", "wrapped"]),
12875
12309
  providerAddonId: string(),
12876
12310
  providerNodeId: string(),
12877
12311
  nativeAddonId: string()
@@ -12880,11 +12314,7 @@ method(object({
12880
12314
  deviceId: number(),
12881
12315
  entries: array(object({
12882
12316
  capName: string(),
12883
- kind: _enum([
12884
- "native",
12885
- "wrapped",
12886
- "linked"
12887
- ]),
12317
+ kind: _enum(["native", "wrapped"]),
12888
12318
  providerAddonId: string(),
12889
12319
  providerNodeId: string(),
12890
12320
  nativeAddonId: string()
@@ -13693,6 +13123,10 @@ var motionDetectionCapability = {
13693
13123
  mode: "singleton",
13694
13124
  kind: "wrapper",
13695
13125
  defaultActive: true,
13126
+ /** Frame-differencing motion analysis over a video stream — only a camera
13127
+ * produces the frames. Read by the `defaultActive` auto-bind in
13128
+ * `device-manager.getBindings` to decide which devices it may claim. */
13129
+ deviceTypes: [DeviceType.Camera],
13696
13130
  exposesDeviceSettings: true,
13697
13131
  methods: {
13698
13132
  analyze: method(object({
@@ -16215,6 +15649,18 @@ var OauthIntegrationDescriptorSchema = object({
16215
15649
  * redirect_uri that does not start with one of these. Required —
16216
15650
  * an empty list means the integration can never complete linking. */
16217
15651
  allowedRedirectPrefixes: array(string()).min(1),
15652
+ /** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
15653
+ * RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
15654
+ * `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
15655
+ * whose address the hub cannot know in advance (a Home Assistant at
15656
+ * `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
15657
+ * exactly; a public host never satisfies this branch, so it is not a
15658
+ * wildcard prefix by another name. */
15659
+ allowedPrivateHostPaths: array(string()).optional(),
15660
+ /** When true this is a PUBLIC client (source is published, no secret can be
15661
+ * protected) and PKCE is mandatory: `/authorize` refuses without an S256
15662
+ * `code_challenge`, `/token` refuses without the matching `code_verifier`. */
15663
+ requiresPkce: boolean().optional(),
16218
15664
  /** Optional public origin (no trailing slash) that this integration's
16219
15665
  * issued codes/tokens should carry as the `hubUrl` claim — typically the
16220
15666
  * operator-selected external-access endpoint resolved by the addon. When
@@ -16365,7 +15811,7 @@ var TrackEnvelopeSchema = object({
16365
15811
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
16366
15812
  * keeps every scalar the list surfaces actually render (ids, class(es),
16367
15813
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
16368
- * zonesVisited, bestEventId, envelope) and returns `positions` /
15814
+ * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
16369
15815
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
16370
15816
  * `getTrack`. Mirrors the event-store `projection` convention
16371
15817
  * (`getObjectEvents` et al.).
@@ -16602,6 +16048,24 @@ var TrackSchema = object({
16602
16048
  * Populated from the persisted envelope columns on historical reads;
16603
16049
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
16604
16050
  envelope: TrackEnvelopeSchema.optional(),
16051
+ /**
16052
+ * A face DETECTOR found a face on this track — nothing more. It says the
16053
+ * detail plane produced a `face` detail; it does NOT say the face was
16054
+ * embedded, matched, above `minFacePx`, or that the recognizer was even
16055
+ * enabled. Set once and never cleared.
16056
+ *
16057
+ * **This exists so "face present but not recognised" is expressible.** A
16058
+ * recognised identity lands in `subLabel` (attributed to the face chain via
16059
+ * `subLabelMeta.stepId`), so before this field a track with an unmatched face
16060
+ * and a track with no face at all were byte-identical on the wire and no
16061
+ * surface could tell them apart. The read is `hasFace === true && subLabel
16062
+ * === undefined`.
16063
+ *
16064
+ * **Absent ≠ false.** Every row written before the column existed omits it,
16065
+ * and so does every server that predates the field — a consumer must test
16066
+ * `=== true` and render nothing otherwise, never infer "no face".
16067
+ */
16068
+ hasFace: boolean().optional(),
16605
16069
  ...TrackFlagFields,
16606
16070
  ...TrackRetrainFields
16607
16071
  });
@@ -19510,6 +18974,10 @@ var SsoBridgeClaimsSchema = object({
19510
18974
  integrationId: string().optional(),
19511
18975
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
19512
18976
  jti: string().optional(),
18977
+ /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
18978
+ * client. Its PRESENCE is what makes the verifier mandatory at exchange,
18979
+ * so the requirement travels with the code and not with mutable config. */
18980
+ codeChallenge: string().optional(),
19513
18981
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
19514
18982
  * tokens so the verify path can check the session is not revoked. */
19515
18983
  sessionId: string().optional()
@@ -20077,7 +19545,7 @@ var ClipPlaybackSchema = object({
20077
19545
  playbackEndpoints: array(string()).optional(),
20078
19546
  token: string().optional()
20079
19547
  });
20080
- method(object({
19548
+ DeviceType.Camera, method(object({
20081
19549
  deviceId: number(),
20082
19550
  since: number(),
20083
19551
  until: number(),
@@ -25333,13 +24801,18 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
25333
24801
  username: string(),
25334
24802
  scopes: array(TokenScopeSchema),
25335
24803
  redirectUri: string(),
25336
- hubUrl: string()
24804
+ hubUrl: string(),
24805
+ /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
24806
+ * that carries one can ONLY be exchanged with the matching verifier. */
24807
+ codeChallenge: string().optional()
25337
24808
  }), object({ code: string() }), {
25338
24809
  kind: "mutation",
25339
24810
  access: "create"
25340
24811
  }), method(object({
25341
24812
  code: string(),
25342
- redirectUri: string()
24813
+ redirectUri: string(),
24814
+ /** PKCE verifier. REQUIRED when the code carries a challenge. */
24815
+ codeVerifier: string().optional()
25343
24816
  }), object({
25344
24817
  accessToken: string(),
25345
24818
  refreshToken: string(),
@@ -25566,190 +25039,819 @@ object({
25566
25039
  });
25567
25040
  DeviceType.Weather;
25568
25041
  /**
25569
- * Per-zone occupancy aggregation produced by the analytics frame
25570
- * processor on every inference result. Covers the full combinatorial
25571
- * matrix the operator UI needs: total objects everywhere, total
25572
- * objects per zone, single class everywhere, single class per zone,
25573
- * objects outside any zone.
25574
- *
25575
- * Counts are derived from the analytics tracker (tracked detections
25576
- * with stable trackIds), not raw detector hits — this filters out
25577
- * one-off detector flickers and gives counts that match what the user
25578
- * sees on the live overlay.
25042
+ * Per-zone occupancy aggregation produced by the analytics frame
25043
+ * processor on every inference result. Covers the full combinatorial
25044
+ * matrix the operator UI needs: total objects everywhere, total
25045
+ * objects per zone, single class everywhere, single class per zone,
25046
+ * objects outside any zone.
25047
+ *
25048
+ * Counts are derived from the analytics tracker (tracked detections
25049
+ * with stable trackIds), not raw detector hits — this filters out
25050
+ * one-off detector flickers and gives counts that match what the user
25051
+ * sees on the live overlay.
25052
+ *
25053
+ * Overlap policy: a detection that intersects two zones counts in
25054
+ * BOTH zones' `byClass` and `totalObjects` (count-in-each). The
25055
+ * `frame` aggregate de-duplicates trivially since it's frame-wide.
25056
+ * `unzoned` counts only detections that landed in zero zones.
25057
+ */
25058
+ var PerScopeBreakdownSchema = object({
25059
+ /** Total tracked objects in this scope (frame / zone / unzoned). */
25060
+ totalObjects: number().int().nonnegative(),
25061
+ /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
25062
+ byClass: record(string(), number().int().nonnegative())
25063
+ });
25064
+ var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
25065
+ zoneId: string(),
25066
+ zoneName: string(),
25067
+ /** TrackIds of objects currently inside this zone — for cross-reference
25068
+ * with the per-track detail panel and live overlay. */
25069
+ trackIds: array(string()).readonly()
25070
+ });
25071
+ /**
25072
+ * A parked ("stationary") object surfaced alongside occupancy — an object that
25073
+ * settled and stopped moving. It is NO LONGER a tracked object (the tracker was
25074
+ * told to forget it so it stops re-spawning tracks/events), but it IS still
25075
+ * physically present, so it keeps counting toward `frame` occupancy and is
25076
+ * listed here so the UI can show it in a dedicated "Stationary" section instead
25077
+ * of flooding the live event feed.
25078
+ */
25079
+ var StationaryObjectSchema = object({
25080
+ id: string(),
25081
+ className: string(),
25082
+ bbox: object({
25083
+ x: number(),
25084
+ y: number(),
25085
+ w: number(),
25086
+ h: number()
25087
+ }),
25088
+ frameWidth: number().int().nonnegative(),
25089
+ frameHeight: number().int().nonnegative(),
25090
+ /** When the source track was first seen. */
25091
+ firstSeenAt: number().int(),
25092
+ /** When the object was recognised as parked (promotion time). */
25093
+ becameStationaryAt: number().int(),
25094
+ /** Last frame a detection confirmed the object is still there. */
25095
+ lastConfirmedAt: number().int(),
25096
+ /** Enrichment label carried from the source track (identity / plate). */
25097
+ label: string().optional(),
25098
+ /** Native-resolution key-frame media key for the parked object's best image. */
25099
+ keyFrameMediaKey: string().optional()
25100
+ });
25101
+ var CameraOccupancySnapshotSchema = object({
25102
+ /** Frame timestamp of the inference result that produced this snapshot. */
25103
+ ts: number().int(),
25104
+ /** Frame width/height in pixels — let the UI normalize bbox coords. */
25105
+ frameWidth: number().int().nonnegative(),
25106
+ frameHeight: number().int().nonnegative(),
25107
+ /** Per-zone breakdown — one entry per defined zone (user + onboard). */
25108
+ zones: array(ZoneScopeBreakdownSchema).readonly(),
25109
+ /** Frame-wide aggregate (everywhere, regardless of zone membership).
25110
+ * INCLUDES currently-confirmed stationary objects (they are still present). */
25111
+ frame: PerScopeBreakdownSchema,
25112
+ /** Detections that landed outside every zone. Empty when no zones defined. */
25113
+ unzoned: PerScopeBreakdownSchema,
25114
+ /** Parked objects on this camera (additive — absent on legacy snapshots).
25115
+ * Surfaced separately so the UI shows them in a dedicated section rather
25116
+ * than as repeated tracks/events. */
25117
+ stationaryObjects: array(StationaryObjectSchema).readonly().optional()
25118
+ });
25119
+ /**
25120
+ * Time-series resolution. The history methods return one bucket per
25121
+ * step over the requested range. Smaller resolutions cost more
25122
+ * memory + bandwidth; bound to discrete steps so caller cannot ask
25123
+ * for arbitrary fractional buckets.
25124
+ */
25125
+ var HistoryResolutionEnum = _enum([
25126
+ "minute",
25127
+ "5min",
25128
+ "hour"
25129
+ ]);
25130
+ var HistoryRangeSchema = object({
25131
+ /** Range start (epoch ms, inclusive). */
25132
+ from: number().int(),
25133
+ /** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
25134
+ to: number().int(),
25135
+ resolution: HistoryResolutionEnum
25136
+ });
25137
+ var HistoryPointSchema = object({
25138
+ /** Bucket midpoint (epoch ms). */
25139
+ ts: number().int(),
25140
+ /** Object count averaged over the bucket (rounded to nearest integer). */
25141
+ count: number().int().nonnegative()
25142
+ });
25143
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
25144
+ deviceId: number(),
25145
+ zoneId: string(),
25146
+ className: string().optional()
25147
+ }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
25148
+ deviceId: number(),
25149
+ className: string().optional()
25150
+ }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
25151
+ deviceId: number(),
25152
+ className: string().optional()
25153
+ }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
25154
+ /**
25155
+ * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
25156
+ * cap so a single CRUD surface backs every consumer; each stage has
25157
+ * its own dev-state mirror slice (`motion-zone-rules`,
25158
+ * `detection-zone-rules`, …) so consumer addons subscribe independently.
25159
+ *
25160
+ * Extend the enum here when a new gating consumer comes online (audio
25161
+ * gating, alert filtering, …) — no other surface needs to change.
25162
+ */
25163
+ var ZoneRuleStageEnum = _enum([
25164
+ "motion",
25165
+ "detection",
25166
+ "package"
25167
+ ]);
25168
+ DeviceType.Camera, method(object({
25169
+ deviceId: number(),
25170
+ stage: ZoneRuleStageEnum
25171
+ }), array(ZoneRuleSchema).readonly()), method(object({
25172
+ deviceId: number(),
25173
+ stage: ZoneRuleStageEnum,
25174
+ rules: array(ZoneRuleSchema).readonly()
25175
+ }), _void(), {
25176
+ kind: "mutation",
25177
+ auth: "admin"
25178
+ }), object({
25179
+ motion: array(ZoneRuleSchema).readonly(),
25180
+ detection: array(ZoneRuleSchema).readonly(),
25181
+ package: array(ZoneRuleSchema).readonly()
25182
+ });
25183
+ var HF_BASE_URL = `https://huggingface.co/camstack/camstack-models/resolve/main`;
25184
+ /**
25185
+ * Accessory device helpers — shared across drivers.
25186
+ *
25187
+ * Many vendor-specific drivers register accessory child devices on
25188
+ * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
25189
+ * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
25190
+ * driver picks the right `DeviceType` + `DeviceRole` explicitly when
25191
+ * spawning, builds a name derived from the parent, and produces a
25192
+ * stableId tied to the parent so boot-restore can reconstruct the
25193
+ * relationship.
25194
+ *
25195
+ * Centralised `(kind → DeviceType)` mapping was dropped on purpose:
25196
+ * drivers may reasonably disagree on the right type for an accessory
25197
+ * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
25198
+ * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
25199
+ * one canonical mapping was over-prescriptive and added a layer of
25200
+ * indirection without saving meaningful code at call sites — the
25201
+ * driver knows its own hardware best.
25202
+ */
25203
+ /**
25204
+ * Subset of `DeviceRole` values that drivers register as child
25205
+ * accessories of a parent device. Sourced verbatim from `DeviceRole`
25206
+ * — `AccessoryKind` is the alias drivers use when building accessory
25207
+ * children, so the call site reads as
25208
+ * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
25209
+ * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
25210
+ * any role works, including non-accessory ones like Doorbell).
25211
+ */
25212
+ var AccessoryKind = {
25213
+ Siren: DeviceRole.Siren,
25214
+ Floodlight: DeviceRole.Floodlight,
25215
+ Spotlight: DeviceRole.Spotlight,
25216
+ PirSensor: DeviceRole.PirSensor,
25217
+ Chime: DeviceRole.Chime,
25218
+ Autotrack: DeviceRole.Autotrack,
25219
+ Nightvision: DeviceRole.Nightvision,
25220
+ PrivacyMask: DeviceRole.PrivacyMask
25221
+ };
25222
+ AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
25223
+ 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;
25224
+ new Set(Object.values(DeviceType));
25225
+ DeviceFeature.BatteryOperated;
25226
+ /**
25227
+ * Error types for the safe expression engine. Two distinct classes so callers
25228
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
25229
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
25230
+ */
25231
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
25232
+ * the failure is anchored to a character (author-facing inline feedback). */
25233
+ var ExpressionParseError = class extends Error {
25234
+ position;
25235
+ constructor(message, position) {
25236
+ super(message);
25237
+ this.name = "ExpressionParseError";
25238
+ this.position = position;
25239
+ }
25240
+ };
25241
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
25242
+ * result, unknown builtin, step-budget exceeded). */
25243
+ var ExpressionEvalError = class extends Error {
25244
+ constructor(message) {
25245
+ super(message);
25246
+ this.name = "ExpressionEvalError";
25247
+ }
25248
+ };
25249
+ /**
25250
+ * Frozen, null-prototype builtin function table for the expression engine
25251
+ * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
25252
+ * parser rejects any callee not in it, and the evaluator gates each call on an
25253
+ * own-property check against it.
25254
+ *
25255
+ * Because the object has a NULL prototype AND is `Object.freeze`d:
25256
+ * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
25257
+ * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
25258
+ * (there is no `Object.prototype` in the chain), so those names are not
25259
+ * callable — they are simply "unknown function" at parse time.
25260
+ *
25261
+ * Every numeric argument is validated as a finite number and every numeric
25262
+ * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
25263
+ * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
25264
+ * closed rather than emitting a garbage value.
25265
+ */
25266
+ function asFiniteNumber(value, name, index) {
25267
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
25268
+ return value;
25269
+ }
25270
+ function asString$1(value, name, index) {
25271
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
25272
+ return value;
25273
+ }
25274
+ function finiteResult(value, name) {
25275
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
25276
+ return value;
25277
+ }
25278
+ function allFiniteNumbers(args, name) {
25279
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
25280
+ }
25281
+ var INF = Number.POSITIVE_INFINITY;
25282
+ var table = {
25283
+ min: {
25284
+ minArgs: 1,
25285
+ maxArgs: INF,
25286
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
25287
+ },
25288
+ max: {
25289
+ minArgs: 1,
25290
+ maxArgs: INF,
25291
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
25292
+ },
25293
+ abs: {
25294
+ minArgs: 1,
25295
+ maxArgs: 1,
25296
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
25297
+ },
25298
+ floor: {
25299
+ minArgs: 1,
25300
+ maxArgs: 1,
25301
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
25302
+ },
25303
+ ceil: {
25304
+ minArgs: 1,
25305
+ maxArgs: 1,
25306
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
25307
+ },
25308
+ sqrt: {
25309
+ minArgs: 1,
25310
+ maxArgs: 1,
25311
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
25312
+ },
25313
+ round: {
25314
+ minArgs: 1,
25315
+ maxArgs: 2,
25316
+ apply: (args) => {
25317
+ const x = asFiniteNumber(args[0], "round", 0);
25318
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
25319
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
25320
+ const factor = 10 ** digits;
25321
+ return finiteResult(Math.round(x * factor) / factor, "round");
25322
+ }
25323
+ },
25324
+ pow: {
25325
+ minArgs: 2,
25326
+ maxArgs: 2,
25327
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
25328
+ },
25329
+ clamp: {
25330
+ minArgs: 3,
25331
+ maxArgs: 3,
25332
+ apply: (args) => {
25333
+ const x = asFiniteNumber(args[0], "clamp", 0);
25334
+ const lo = asFiniteNumber(args[1], "clamp", 1);
25335
+ const hi = asFiniteNumber(args[2], "clamp", 2);
25336
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
25337
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
25338
+ }
25339
+ },
25340
+ avg: {
25341
+ minArgs: 1,
25342
+ maxArgs: INF,
25343
+ apply: (args) => {
25344
+ const nums = allFiniteNumbers(args, "avg");
25345
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
25346
+ }
25347
+ },
25348
+ sum: {
25349
+ minArgs: 1,
25350
+ maxArgs: INF,
25351
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
25352
+ },
25353
+ coalesce: {
25354
+ minArgs: 1,
25355
+ maxArgs: INF,
25356
+ apply: (args) => {
25357
+ for (const a of args) if (a !== null) return a;
25358
+ return null;
25359
+ }
25360
+ },
25361
+ age: {
25362
+ minArgs: 2,
25363
+ maxArgs: 2,
25364
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
25365
+ },
25366
+ convert: {
25367
+ minArgs: 3,
25368
+ maxArgs: 3,
25369
+ apply: (args, hooks) => {
25370
+ const x = asFiniteNumber(args[0], "convert", 0);
25371
+ const from = asString$1(args[1], "convert", 1).trim();
25372
+ const to = asString$1(args[2], "convert", 2).trim();
25373
+ if (hooks.convert) {
25374
+ const out = hooks.convert(x, from, to);
25375
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
25376
+ return finiteResult(out, "convert");
25377
+ }
25378
+ if (from === to) return x;
25379
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
25380
+ }
25381
+ }
25382
+ };
25383
+ Object.freeze(Object.assign(Object.create(null), table));
25384
+ /** The set of valid builtin names — used by the parser to reject unknown
25385
+ * callees at parse time (immediate author feedback). */
25386
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
25387
+ /**
25388
+ * Resource-bound constants for the safe expression engine.
25389
+ *
25390
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
25391
+ * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
25392
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
25393
+ * work a single author-supplied expression can request, so a hostile or
25394
+ * accidental pathological string can never spend unbounded CPU/memory.
25395
+ */
25396
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
25397
+ * rejected without allocation. */
25398
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
25399
+ /** A legal binding / identifier name. */
25400
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
25401
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
25402
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
25403
+ var RESERVED_BINDING_NAMES = new Set([
25404
+ "now",
25405
+ "true",
25406
+ "false",
25407
+ "null"
25408
+ ]);
25409
+ /**
25410
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
25411
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
25412
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
25413
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
25414
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
25415
+ * is a parse error with a source position, so member access / assignment /
25416
+ * template literals are lexically impossible.
25417
+ */
25418
+ var KEYWORDS = new Set([
25419
+ "true",
25420
+ "false",
25421
+ "null"
25422
+ ]);
25423
+ function isDigit(ch) {
25424
+ return ch >= "0" && ch <= "9";
25425
+ }
25426
+ function isIdentStart(ch) {
25427
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
25428
+ }
25429
+ function isIdentPart(ch) {
25430
+ return isIdentStart(ch) || isDigit(ch);
25431
+ }
25432
+ function isWhitespace(ch) {
25433
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
25434
+ }
25435
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
25436
+ * Throws `ExpressionParseError` on any illegal character or unterminated
25437
+ * string. */
25438
+ function tokenize(source) {
25439
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
25440
+ const tokens = [];
25441
+ let i = 0;
25442
+ const n = source.length;
25443
+ while (i < n) {
25444
+ const ch = source[i];
25445
+ if (isWhitespace(ch)) {
25446
+ i += 1;
25447
+ continue;
25448
+ }
25449
+ if (isDigit(ch)) {
25450
+ const start = i;
25451
+ while (i < n && isDigit(source[i])) i += 1;
25452
+ if (i < n && source[i] === ".") {
25453
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
25454
+ i += 1;
25455
+ while (i < n && isDigit(source[i])) i += 1;
25456
+ }
25457
+ const text = source.slice(start, i);
25458
+ const value = Number(text);
25459
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
25460
+ tokens.push({
25461
+ type: "number",
25462
+ value,
25463
+ pos: start
25464
+ });
25465
+ continue;
25466
+ }
25467
+ if (ch === "'" || ch === "\"") {
25468
+ const quote = ch;
25469
+ const start = i;
25470
+ i += 1;
25471
+ let out = "";
25472
+ let closed = false;
25473
+ while (i < n) {
25474
+ const c = source[i];
25475
+ if (c === "\\") {
25476
+ const next = i + 1 < n ? source[i + 1] : "";
25477
+ if (next === "\\" || next === "'" || next === "\"") {
25478
+ out += next;
25479
+ i += 2;
25480
+ continue;
25481
+ }
25482
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
25483
+ }
25484
+ if (c === quote) {
25485
+ closed = true;
25486
+ i += 1;
25487
+ break;
25488
+ }
25489
+ out += c;
25490
+ i += 1;
25491
+ }
25492
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
25493
+ tokens.push({
25494
+ type: "string",
25495
+ value: out,
25496
+ pos: start
25497
+ });
25498
+ continue;
25499
+ }
25500
+ if (isIdentStart(ch)) {
25501
+ const start = i;
25502
+ while (i < n && isIdentPart(source[i])) i += 1;
25503
+ const text = source.slice(start, i);
25504
+ if (KEYWORDS.has(text)) tokens.push({
25505
+ type: "keyword",
25506
+ keyword: keywordOf(text),
25507
+ pos: start
25508
+ });
25509
+ else tokens.push({
25510
+ type: "identifier",
25511
+ name: text,
25512
+ pos: start
25513
+ });
25514
+ continue;
25515
+ }
25516
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
25517
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
25518
+ tokens.push({
25519
+ type: "punct",
25520
+ punct: two,
25521
+ pos: i
25522
+ });
25523
+ i += 2;
25524
+ continue;
25525
+ }
25526
+ if (isSinglePunct(ch)) {
25527
+ tokens.push({
25528
+ type: "punct",
25529
+ punct: ch,
25530
+ pos: i
25531
+ });
25532
+ i += 1;
25533
+ continue;
25534
+ }
25535
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
25536
+ }
25537
+ tokens.push({
25538
+ type: "eof",
25539
+ pos: n
25540
+ });
25541
+ return tokens;
25542
+ }
25543
+ function keywordOf(text) {
25544
+ if (text === "true") return "true";
25545
+ if (text === "false") return "false";
25546
+ return "null";
25547
+ }
25548
+ function isSinglePunct(ch) {
25549
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
25550
+ }
25551
+ /**
25552
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
25553
+ *
25554
+ * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
25555
+ * → relational → additive → multiplicative → unary `! -` → call / primary.
25556
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
25557
+ * string validated against the builtin table at parse time, so an unknown
25558
+ * function is rejected immediately (author feedback) and a persisted expression
25559
+ * that references a since-removed builtin degrades at read.
25560
+ *
25561
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
25562
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
25563
+ */
25564
+ /** Binary/logical operator precedence (higher binds tighter). */
25565
+ var BINARY_PRECEDENCE = {
25566
+ "||": 1,
25567
+ "&&": 2,
25568
+ "==": 3,
25569
+ "!=": 3,
25570
+ "<": 4,
25571
+ "<=": 4,
25572
+ ">": 4,
25573
+ ">=": 4,
25574
+ "+": 5,
25575
+ "-": 5,
25576
+ "*": 6,
25577
+ "/": 6,
25578
+ "%": 6
25579
+ };
25580
+ function isLogicalOp(op) {
25581
+ return op === "&&" || op === "||";
25582
+ }
25583
+ function isBinaryOp(op) {
25584
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
25585
+ }
25586
+ var Parser = class {
25587
+ tokens;
25588
+ pos = 0;
25589
+ nodeCount = 0;
25590
+ identifiers = /* @__PURE__ */ new Set();
25591
+ callees = /* @__PURE__ */ new Set();
25592
+ constructor(tokens) {
25593
+ this.tokens = tokens;
25594
+ }
25595
+ parse() {
25596
+ const ast = this.parseTernary();
25597
+ const tok = this.peek();
25598
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
25599
+ return {
25600
+ ast,
25601
+ identifiers: this.identifiers,
25602
+ callees: this.callees,
25603
+ nodeCount: this.nodeCount
25604
+ };
25605
+ }
25606
+ peek() {
25607
+ return this.tokens[this.pos];
25608
+ }
25609
+ next() {
25610
+ return this.tokens[this.pos++];
25611
+ }
25612
+ /** Consume a punctuator token, erroring if the next token isn't it. */
25613
+ expectPunct(punct) {
25614
+ const tok = this.peek();
25615
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
25616
+ this.pos += 1;
25617
+ }
25618
+ matchPunct(punct) {
25619
+ const tok = this.peek();
25620
+ if (tok.type === "punct" && tok.punct === punct) {
25621
+ this.pos += 1;
25622
+ return true;
25623
+ }
25624
+ return false;
25625
+ }
25626
+ countNode() {
25627
+ this.nodeCount += 1;
25628
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
25629
+ }
25630
+ parseTernary() {
25631
+ const test = this.parseBinary(1);
25632
+ if (this.matchPunct("?")) {
25633
+ const consequent = this.parseTernary();
25634
+ this.expectPunct(":");
25635
+ const alternate = this.parseTernary();
25636
+ this.countNode();
25637
+ return {
25638
+ kind: "conditional",
25639
+ test,
25640
+ consequent,
25641
+ alternate
25642
+ };
25643
+ }
25644
+ return test;
25645
+ }
25646
+ parseBinary(minPrec) {
25647
+ let left = this.parseUnary();
25648
+ for (;;) {
25649
+ const tok = this.peek();
25650
+ if (tok.type !== "punct") break;
25651
+ const prec = BINARY_PRECEDENCE[tok.punct];
25652
+ if (prec === void 0 || prec < minPrec) break;
25653
+ const op = tok.punct;
25654
+ this.pos += 1;
25655
+ const right = this.parseBinary(prec + 1);
25656
+ this.countNode();
25657
+ if (isLogicalOp(op)) left = {
25658
+ kind: "logical",
25659
+ op,
25660
+ left,
25661
+ right
25662
+ };
25663
+ else if (isBinaryOp(op)) left = {
25664
+ kind: "binary",
25665
+ op,
25666
+ left,
25667
+ right
25668
+ };
25669
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
25670
+ }
25671
+ return left;
25672
+ }
25673
+ parseUnary() {
25674
+ const tok = this.peek();
25675
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
25676
+ const op = tok.punct;
25677
+ this.pos += 1;
25678
+ const operand = this.parseUnary();
25679
+ this.countNode();
25680
+ return {
25681
+ kind: "unary",
25682
+ op,
25683
+ operand
25684
+ };
25685
+ }
25686
+ return this.parsePrimary();
25687
+ }
25688
+ parsePrimary() {
25689
+ const tok = this.next();
25690
+ switch (tok.type) {
25691
+ case "number":
25692
+ this.countNode();
25693
+ return {
25694
+ kind: "literal",
25695
+ value: tok.value
25696
+ };
25697
+ case "string":
25698
+ this.countNode();
25699
+ return {
25700
+ kind: "literal",
25701
+ value: tok.value
25702
+ };
25703
+ case "keyword":
25704
+ this.countNode();
25705
+ return {
25706
+ kind: "literal",
25707
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
25708
+ };
25709
+ case "identifier": {
25710
+ const nextTok = this.peek();
25711
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
25712
+ this.identifiers.add(tok.name);
25713
+ this.countNode();
25714
+ return {
25715
+ kind: "identifier",
25716
+ name: tok.name
25717
+ };
25718
+ }
25719
+ case "punct":
25720
+ if (tok.punct === "(") {
25721
+ const inner = this.parseTernary();
25722
+ this.expectPunct(")");
25723
+ return inner;
25724
+ }
25725
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
25726
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
25727
+ }
25728
+ }
25729
+ parseCall(callee, pos) {
25730
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
25731
+ this.expectPunct("(");
25732
+ const args = [];
25733
+ if (!this.matchPunct(")")) for (;;) {
25734
+ args.push(this.parseTernary());
25735
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
25736
+ if (this.matchPunct(",")) continue;
25737
+ this.expectPunct(")");
25738
+ break;
25739
+ }
25740
+ this.callees.add(callee);
25741
+ this.countNode();
25742
+ return {
25743
+ kind: "call",
25744
+ callee,
25745
+ args
25746
+ };
25747
+ }
25748
+ };
25749
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
25750
+ * `ExpressionParseError` on any lexical or grammatical failure. */
25751
+ function parseExpression(source) {
25752
+ return new Parser(tokenize(source)).parse();
25753
+ }
25754
+ /**
25755
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
25756
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
25757
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
25758
+ * one per read on a hot resolve path.
25579
25759
  *
25580
- * Overlap policy: a detection that intersects two zones counts in
25581
- * BOTH zones' `byClass` and `totalObjects` (count-in-each). The
25582
- * `frame` aggregate de-duplicates trivially since it's frame-wide.
25583
- * `unzoned` counts only detections that landed in zero zones.
25760
+ * The cache is a module-level singleton: entries are pure, content-addressed
25761
+ * ASTs keyed by the raw source string, so sharing one instance across all
25762
+ * callers is safe and maximises hit rate.
25584
25763
  */
25585
- var PerScopeBreakdownSchema = object({
25586
- /** Total tracked objects in this scope (frame / zone / unzoned). */
25587
- totalObjects: number().int().nonnegative(),
25588
- /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
25589
- byClass: record(string(), number().int().nonnegative())
25590
- });
25591
- var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
25592
- zoneId: string(),
25593
- zoneName: string(),
25594
- /** TrackIds of objects currently inside this zone — for cross-reference
25595
- * with the per-track detail panel and live overlay. */
25596
- trackIds: array(string()).readonly()
25597
- });
25764
+ var cache = /* @__PURE__ */ new Map();
25765
+ function getCached(source) {
25766
+ const hit = cache.get(source);
25767
+ if (hit !== void 0) {
25768
+ cache.delete(source);
25769
+ cache.set(source, hit);
25770
+ return hit;
25771
+ }
25772
+ let result;
25773
+ try {
25774
+ result = {
25775
+ ok: true,
25776
+ parsed: parseExpression(source)
25777
+ };
25778
+ } catch (err) {
25779
+ result = {
25780
+ ok: false,
25781
+ error: err instanceof ExpressionParseError ? err.message : String(err)
25782
+ };
25783
+ }
25784
+ cache.set(source, result);
25785
+ if (cache.size > 256) {
25786
+ const oldest = cache.keys().next().value;
25787
+ if (oldest !== void 0) cache.delete(oldest);
25788
+ }
25789
+ return result;
25790
+ }
25791
+ /** Compile `source`, returning a discriminated result instead of throwing.
25792
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
25793
+ function compileExpressionSafe(source) {
25794
+ return getCached(source);
25795
+ }
25796
+ Object.freeze({});
25598
25797
  /**
25599
- * A parked ("stationary") object surfaced alongside occupancy an object that
25600
- * settled and stopped moving. It is NO LONGER a tracked object (the tracker was
25601
- * told to forget it so it stops re-spawning tracks/events), but it IS still
25602
- * physically present, so it keeps counting toward `frame` occupancy and is
25603
- * listed here so the UI can show it in a dedicated "Stationary" section instead
25604
- * of flooding the live event feed.
25798
+ * Author-time validation. Returns `null` when the source is valid, else a
25799
+ * human-readable error message. Checks: the expression compiles; binding count
25800
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
25801
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
25802
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
25605
25803
  */
25606
- var StationaryObjectSchema = object({
25607
- id: string(),
25608
- className: string(),
25609
- bbox: object({
25610
- x: number(),
25611
- y: number(),
25612
- w: number(),
25613
- h: number()
25804
+ function validateExpressionSource(src) {
25805
+ const names = Object.keys(src.bindings);
25806
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
25807
+ for (const name of names) {
25808
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
25809
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
25810
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
25811
+ }
25812
+ const compiled = compileExpressionSafe(src.expr);
25813
+ if (!compiled.ok) return compiled.error;
25814
+ const bound = new Set(names);
25815
+ for (const id of compiled.parsed.identifiers) {
25816
+ if (id === "now") continue;
25817
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
25818
+ }
25819
+ return null;
25820
+ }
25821
+ var ExpressionBindingSourceSchema = union([
25822
+ object({
25823
+ kind: literal("field").optional(),
25824
+ sourceKey: string(),
25825
+ cap: string(),
25826
+ fieldPath: string()
25614
25827
  }),
25615
- frameWidth: number().int().nonnegative(),
25616
- frameHeight: number().int().nonnegative(),
25617
- /** When the source track was first seen. */
25618
- firstSeenAt: number().int(),
25619
- /** When the object was recognised as parked (promotion time). */
25620
- becameStationaryAt: number().int(),
25621
- /** Last frame a detection confirmed the object is still there. */
25622
- lastConfirmedAt: number().int(),
25623
- /** Enrichment label carried from the source track (identity / plate). */
25624
- label: string().optional(),
25625
- /** Native-resolution key-frame media key for the parked object's best image. */
25626
- keyFrameMediaKey: string().optional()
25627
- });
25628
- var CameraOccupancySnapshotSchema = object({
25629
- /** Frame timestamp of the inference result that produced this snapshot. */
25630
- ts: number().int(),
25631
- /** Frame width/height in pixels — let the UI normalize bbox coords. */
25632
- frameWidth: number().int().nonnegative(),
25633
- frameHeight: number().int().nonnegative(),
25634
- /** Per-zone breakdown — one entry per defined zone (user + onboard). */
25635
- zones: array(ZoneScopeBreakdownSchema).readonly(),
25636
- /** Frame-wide aggregate (everywhere, regardless of zone membership).
25637
- * INCLUDES currently-confirmed stationary objects (they are still present). */
25638
- frame: PerScopeBreakdownSchema,
25639
- /** Detections that landed outside every zone. Empty when no zones defined. */
25640
- unzoned: PerScopeBreakdownSchema,
25641
- /** Parked objects on this camera (additive — absent on legacy snapshots).
25642
- * Surfaced separately so the UI shows them in a dedicated section rather
25643
- * than as repeated tracks/events. */
25644
- stationaryObjects: array(StationaryObjectSchema).readonly().optional()
25645
- });
25646
- /**
25647
- * Time-series resolution. The history methods return one bucket per
25648
- * step over the requested range. Smaller resolutions cost more
25649
- * memory + bandwidth; bound to discrete steps so caller cannot ask
25650
- * for arbitrary fractional buckets.
25651
- */
25652
- var HistoryResolutionEnum = _enum([
25653
- "minute",
25654
- "5min",
25655
- "hour"
25656
- ]);
25657
- var HistoryRangeSchema = object({
25658
- /** Range start (epoch ms, inclusive). */
25659
- from: number().int(),
25660
- /** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
25661
- to: number().int(),
25662
- resolution: HistoryResolutionEnum
25663
- });
25664
- var HistoryPointSchema = object({
25665
- /** Bucket midpoint (epoch ms). */
25666
- ts: number().int(),
25667
- /** Object count averaged over the bucket (rounded to nearest integer). */
25668
- count: number().int().nonnegative()
25669
- });
25670
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
25671
- deviceId: number(),
25672
- zoneId: string(),
25673
- className: string().optional()
25674
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
25675
- deviceId: number(),
25676
- className: string().optional()
25677
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
25678
- deviceId: number(),
25679
- className: string().optional()
25680
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
25681
- /**
25682
- * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
25683
- * cap so a single CRUD surface backs every consumer; each stage has
25684
- * its own dev-state mirror slice (`motion-zone-rules`,
25685
- * `detection-zone-rules`, …) so consumer addons subscribe independently.
25686
- *
25687
- * Extend the enum here when a new gating consumer comes online (audio
25688
- * gating, alert filtering, …) — no other surface needs to change.
25689
- */
25690
- var ZoneRuleStageEnum = _enum([
25691
- "motion",
25692
- "detection",
25693
- "package"
25828
+ object({
25829
+ kind: literal("literal"),
25830
+ value: union([
25831
+ string(),
25832
+ number(),
25833
+ boolean(),
25834
+ _null()
25835
+ ])
25836
+ }),
25837
+ object({
25838
+ kind: literal("global"),
25839
+ sourceStableId: string(),
25840
+ cap: string(),
25841
+ fieldPath: string()
25842
+ })
25694
25843
  ]);
25695
- DeviceType.Camera, method(object({
25696
- deviceId: number(),
25697
- stage: ZoneRuleStageEnum
25698
- }), array(ZoneRuleSchema).readonly()), method(object({
25699
- deviceId: number(),
25700
- stage: ZoneRuleStageEnum,
25701
- rules: array(ZoneRuleSchema).readonly()
25702
- }), _void(), {
25703
- kind: "mutation",
25704
- auth: "admin"
25705
- }), object({
25706
- motion: array(ZoneRuleSchema).readonly(),
25707
- detection: array(ZoneRuleSchema).readonly(),
25708
- package: array(ZoneRuleSchema).readonly()
25709
- });
25710
- var HF_BASE_URL = `https://huggingface.co/camstack/camstack-models/resolve/main`;
25711
- /**
25712
- * Accessory device helpers — shared across drivers.
25713
- *
25714
- * Many vendor-specific drivers register accessory child devices on
25715
- * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
25716
- * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
25717
- * driver picks the right `DeviceType` + `DeviceRole` explicitly when
25718
- * spawning, builds a name derived from the parent, and produces a
25719
- * stableId tied to the parent so boot-restore can reconstruct the
25720
- * relationship.
25721
- *
25722
- * Centralised `(kind → DeviceType)` mapping was dropped on purpose:
25723
- * drivers may reasonably disagree on the right type for an accessory
25724
- * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
25725
- * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
25726
- * one canonical mapping was over-prescriptive and added a layer of
25727
- * indirection without saving meaningful code at call sites — the
25728
- * driver knows its own hardware best.
25729
- */
25730
- /**
25731
- * Subset of `DeviceRole` values that drivers register as child
25732
- * accessories of a parent device. Sourced verbatim from `DeviceRole`
25733
- * — `AccessoryKind` is the alias drivers use when building accessory
25734
- * children, so the call site reads as
25735
- * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
25736
- * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
25737
- * any role works, including non-accessory ones like Doorbell).
25738
- */
25739
- var AccessoryKind = {
25740
- Siren: DeviceRole.Siren,
25741
- Floodlight: DeviceRole.Floodlight,
25742
- Spotlight: DeviceRole.Spotlight,
25743
- PirSensor: DeviceRole.PirSensor,
25744
- Chime: DeviceRole.Chime,
25745
- Autotrack: DeviceRole.Autotrack,
25746
- Nightvision: DeviceRole.Nightvision,
25747
- PrivacyMask: DeviceRole.PrivacyMask
25748
- };
25749
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
25750
- 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;
25751
- new Set(Object.values(DeviceType));
25752
- DeviceFeature.BatteryOperated;
25844
+ object({
25845
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
25846
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
25847
+ }).superRefine((src, ctx) => {
25848
+ const err = validateExpressionSource(src);
25849
+ if (err !== null) ctx.addIssue({
25850
+ code: "custom",
25851
+ message: err,
25852
+ path: ["expr"]
25853
+ });
25854
+ });
25753
25855
  Object.freeze({
25754
25856
  "accessories.setChildHidden": {
25755
25857
  capName: "accessories",
@@ -26573,6 +26675,12 @@ Object.freeze({
26573
26675
  addonId: null,
26574
26676
  access: "view"
26575
26677
  },
26678
+ "coreBlocks.restart": {
26679
+ capName: "core-blocks",
26680
+ capScope: "system",
26681
+ addonId: null,
26682
+ access: "create"
26683
+ },
26576
26684
  "coreBlocks.setEnabled": {
26577
26685
  capName: "core-blocks",
26578
26686
  capScope: "system",
@@ -27209,12 +27317,6 @@ Object.freeze({
27209
27317
  addonId: null,
27210
27318
  access: "create"
27211
27319
  },
27212
- "deviceManager.setDeviceLinks": {
27213
- capName: "device-manager",
27214
- capScope: "system",
27215
- addonId: null,
27216
- access: "create"
27217
- },
27218
27320
  "deviceManager.setDisabled": {
27219
27321
  capName: "device-manager",
27220
27322
  capScope: "system",
@@ -30359,6 +30461,12 @@ Object.freeze({
30359
30461
  addonId: null,
30360
30462
  access: "create"
30361
30463
  },
30464
+ "streamBroker.fetchEventMedia": {
30465
+ capName: "stream-broker",
30466
+ capScope: "system",
30467
+ addonId: null,
30468
+ access: "create"
30469
+ },
30362
30470
  "streamBroker.getAllRtspEntries": {
30363
30471
  capName: "stream-broker",
30364
30472
  capScope: "system",
@@ -30443,6 +30551,12 @@ Object.freeze({
30443
30551
  addonId: null,
30444
30552
  access: "create"
30445
30553
  },
30554
+ "streamBroker.produceEventMedia": {
30555
+ capName: "stream-broker",
30556
+ capScope: "system",
30557
+ addonId: null,
30558
+ access: "create"
30559
+ },
30446
30560
  "streamBroker.publishCameraStream": {
30447
30561
  capName: "stream-broker",
30448
30562
  capScope: "system",
@@ -31183,249 +31297,62 @@ var DeviceIdsField = array(number()).min(1);
31183
31297
  var CadenceSecField = number().int().min(2).max(3600);
31184
31298
  var FramerateField = number().int().min(1).max(60);
31185
31299
  var TargetsField = array(NcRuleTargetSchema).min(1);
31186
- var PriorityField = number().int().min(1).max(5);
31187
- /**
31188
- * Client-supplied timelapse-rule fields. The server stamps id / createdBy /
31189
- * createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
31190
- * here (see the ownership note above).
31191
- */
31192
- var TimelapseRuleInputSchema = object({
31193
- name: NameField,
31194
- enabled: boolean().default(true),
31195
- /** Cameras sampled by this rule — one scratch dir + one artifact per device. */
31196
- deviceIds: DeviceIdsField,
31197
- /**
31198
- * Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
31199
- * means "always active"): a timelapse is defined by its window boundaries —
31200
- * open clears the scratch, close assembles and delivers.
31201
- */
31202
- schedule: NcScheduleSchema,
31203
- /** Force-snapshot cadence inside the window, seconds (predecessor parity). */
31204
- cadenceSec: CadenceSecField.default(15),
31205
- /** Output frames per second of the assembled mp4 (predecessor parity). */
31206
- framerate: FramerateField.default(10),
31207
- /** `notification-output` targets the finished video/thumbnail is sent to. */
31208
- targets: TargetsField,
31209
- template: TimelapseTemplateSchema.optional(),
31210
- /** Canonical notification priority ordinal (1..5); per-target overridable. */
31211
- priority: PriorityField.default(3)
31212
- });
31213
- object({
31214
- name: NameField.optional(),
31215
- enabled: boolean().optional(),
31216
- deviceIds: DeviceIdsField.optional(),
31217
- schedule: NcScheduleSchema.optional(),
31218
- cadenceSec: CadenceSecField.optional(),
31219
- framerate: FramerateField.optional(),
31220
- targets: TargetsField.optional(),
31221
- template: TimelapseTemplateSchema.nullable().optional(),
31222
- priority: PriorityField.optional()
31223
- });
31224
- TimelapseRuleInputSchema.extend({
31225
- id: string(),
31226
- /**
31227
- * Ownership/visibility key. Absent = admin/global rule (visible to all).
31228
- * Present = personal rule owned by this userId. Server-stamped from the
31229
- * resolved caller; never trusted from a client payload.
31230
- */
31231
- ownerUserId: string().optional(),
31232
- /**
31233
- * Epoch-ms of the last successful generation — the 1-hour re-generation
31234
- * guard's durable state (predecessor parity). Absent = never generated.
31235
- */
31236
- lastGeneratedAt: number().optional(),
31237
- /** userId of the caller who created the rule (server-stamped). */
31238
- createdBy: string(),
31239
- createdAt: number(),
31240
- updatedAt: number()
31241
- });
31242
- function hfModelUrl(repo, path) {
31243
- return `https://huggingface.co/${repo}/resolve/main/${path}`;
31244
- }
31245
- /**
31246
- * Names that, when used as URL query parameters, almost certainly carry
31247
- * a secret. Matching is case-insensitive and anchored to the full param
31248
- * name to avoid false positives like `user_id` (an entity reference, not
31249
- * a credential) or `topic=password-reset` (a value happens to contain
31250
- * the substring "password" but the param name is unrelated).
31251
- *
31252
- * Add new patterns here as we encounter cameras/APIs that leak through
31253
- * the existing list.
31254
- */
31255
- var CREDENTIAL_PARAM_PATTERNS = [
31256
- /^user$/i,
31257
- /^usr$/i,
31258
- /^username$/i,
31259
- /^password$/i,
31260
- /^pwd$/i,
31261
- /^pass$/i,
31262
- /^passwd$/i,
31263
- /^secret$/i,
31264
- /^token$/i,
31265
- /^auth$/i,
31266
- /^auth[_-]?token$/i,
31267
- /^bearer$/i,
31268
- /^api[_-]?key$/i,
31269
- /^access[_-]?token$/i,
31270
- /^refresh[_-]?token$/i
31271
- ];
31272
- function isCredentialParam(name) {
31273
- return CREDENTIAL_PARAM_PATTERNS.some((re) => re.test(name));
31274
- }
31275
- /**
31276
- * Redact secrets we may surface in logs or telemetry:
31277
- * 1. `userinfo` (`user:password@host`) — the classic RTSP shape.
31278
- * 2. Query-string credential params (`?user=...&password=...`) — the
31279
- * Reolink RTMP shape and several HTTP-pushed-stream APIs.
31280
- *
31281
- * Returns the original string verbatim when the input is not a parseable
31282
- * URL — a log helper must never throw. Param values are replaced with
31283
- * `***` so an operator reading logs still sees WHICH credential was
31284
- * present (useful for debugging "wrong password" issues) without
31285
- * exposing the value.
31286
- *
31287
- * The credential-name match is case-insensitive but anchored to the
31288
- * whole param name; values like `user_id=42` or `topic=password-reset`
31289
- * are NOT masked (those are entity references, not credentials).
31290
- */
31291
- function maskUrlCredentials(rawUrl) {
31292
- try {
31293
- const u = new URL(rawUrl);
31294
- let touched = false;
31295
- if (u.username || u.password) {
31296
- u.username = "";
31297
- u.password = "";
31298
- touched = true;
31299
- }
31300
- for (const key of Array.from(u.searchParams.keys())) if (isCredentialParam(key)) {
31301
- u.searchParams.set(key, "***");
31302
- touched = true;
31303
- }
31304
- return touched ? u.toString() : rawUrl;
31305
- } catch {
31306
- return rawUrl;
31307
- }
31308
- }
31309
- /**
31310
- * Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
31311
- * drain() returns up to maxCount items in FIFO order and removes them.
31312
- */
31313
- var RingBuffer = class {
31314
- capacity;
31315
- items;
31316
- head = 0;
31317
- tail = 0;
31318
- count = 0;
31319
- constructor(capacity) {
31320
- this.capacity = capacity;
31321
- this.items = Array.from({ length: capacity });
31322
- }
31323
- get size() {
31324
- return this.count;
31325
- }
31326
- push(item) {
31327
- this.items[this.tail] = item;
31328
- this.tail = (this.tail + 1) % this.capacity;
31329
- if (this.count < this.capacity) this.count++;
31330
- else this.head = (this.head + 1) % this.capacity;
31331
- }
31332
- drain(maxCount) {
31333
- const result = [];
31334
- const n = Math.min(maxCount, this.count);
31335
- for (let i = 0; i < n; i++) {
31336
- result.push(this.items[this.head]);
31337
- this.items[this.head] = void 0;
31338
- this.head = (this.head + 1) % this.capacity;
31339
- }
31340
- this.count -= n;
31341
- return result;
31342
- }
31343
- };
31344
- /**
31345
- * DEVICE-POOL backend → model format — the SSOT for the multi-device inference
31346
- * DESCRIPTOR path (Python per-device pools), distinct from {@link BACKEND_TO_FORMAT}
31347
- * above which is the legacy NODE runtime map (where `coreml → onnx` because
31348
- * onnxruntime-node's CoreML EP loads `.onnx`). Here a device pool runs the
31349
- * backend NATIVELY, so `coreml → coreml` and `edgetpu → tflite`. Consumed by
31350
- * `resolveDeviceEngine` (addon-pipeline) and the orchestrator's
31351
- * `parseDeviceKey`/`getNodeInferenceDevices` — the previously-duplicated
31352
- * `BACKEND_FORMAT` maps (R3/node-F2). `cpu`/`cuda` floor to onnx.
31353
- */
31354
- var DEVICE_BACKEND_TO_FORMAT = {
31355
- openvino: "openvino",
31356
- edgetpu: "tflite",
31357
- coreml: "coreml",
31358
- onnx: "onnx",
31359
- cpu: "onnx",
31360
- cuda: "onnx"
31361
- };
31362
- /**
31363
- * Evaluate `rules` against `items`. Returns the items partitioned
31364
- * into `passed` (kept by the gate) and `excluded` (dropped). When
31365
- * `rules` is empty or `zones` is empty everything passes — the gate
31366
- * is "off" without explicit configuration.
31367
- *
31368
- * `getCenter` returns the item's normalised (0–1) center point.
31369
- * `getClassName` is optional — when omitted, every rule's
31370
- * `classFilter` is treated as matching all classes (motion-stage
31371
- * semantics). Provide it for object detections so per-class rules
31372
- * narrow correctly.
31373
- */
31374
- function evaluateZoneRules(items, zones, rules, getCenter, getClassName) {
31375
- if (rules.length === 0 || zones.length === 0) return {
31376
- passed: items,
31377
- excluded: []
31378
- };
31379
- const activeRules = rules.filter((r) => r.enabled !== false);
31380
- const includeRules = activeRules.filter((r) => r.mode === "include");
31381
- const excludeRules = activeRules.filter((r) => r.mode === "exclude");
31382
- const whitelistMode = includeRules.length > 0;
31383
- const zonesById = new Map(zones.map((z) => [z.id, z]));
31384
- const passed = [];
31385
- const excluded = [];
31386
- for (const item of items) {
31387
- const center = getCenter(item);
31388
- const className = getClassName?.(item);
31389
- const inInclude = includeRules.some((r) => ruleApplies(r, center, className, zonesById));
31390
- const inExclude = excludeRules.some((r) => ruleApplies(r, center, className, zonesById));
31391
- if (whitelistMode) if (inInclude && !inExclude) passed.push(item);
31392
- else excluded.push(item);
31393
- else if (inExclude) excluded.push(item);
31394
- else passed.push(item);
31395
- }
31396
- return {
31397
- passed,
31398
- excluded
31399
- };
31400
- }
31401
- function ruleApplies(rule, center, className, zonesById) {
31402
- if (rule.classFilter && rule.classFilter.length > 0) {
31403
- if (className !== void 0 && !rule.classFilter.includes(className)) return false;
31404
- }
31405
- for (const zoneId of rule.zoneIds) {
31406
- const zone = zonesById.get(zoneId);
31407
- if (!zone) continue;
31408
- if (pointInPolygon(center, zone.polygon)) return true;
31409
- }
31410
- return false;
31411
- }
31300
+ var PriorityField = number().int().min(1).max(5);
31412
31301
  /**
31413
- * Standard ray-casting point-in-polygon. Polygon vertices are
31414
- * normalised (0–1); the test point is tooframe-resolution-
31415
- * agnostic. Boundary points count as inside (treats `<` as `<=` on
31416
- * the y-edge crossing).
31302
+ * Client-supplied timelapse-rule fields. The server stamps id / createdBy /
31303
+ * createdAt / updatedAt / ownerUserId / lastGeneratedAtnone of them appear
31304
+ * here (see the ownership note above).
31417
31305
  */
31418
- function pointInPolygon(point, polygon) {
31419
- if (polygon.length < 3) return false;
31420
- let inside = false;
31421
- const { x, y } = point;
31422
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
31423
- const a = polygon[i];
31424
- const b = polygon[j];
31425
- if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y || Number.EPSILON) + a.x) inside = !inside;
31426
- }
31427
- return inside;
31428
- }
31306
+ var TimelapseRuleInputSchema = object({
31307
+ name: NameField,
31308
+ enabled: boolean().default(true),
31309
+ /** Cameras sampled by this rule — one scratch dir + one artifact per device. */
31310
+ deviceIds: DeviceIdsField,
31311
+ /**
31312
+ * Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
31313
+ * means "always active"): a timelapse is defined by its window boundaries
31314
+ * open clears the scratch, close assembles and delivers.
31315
+ */
31316
+ schedule: NcScheduleSchema,
31317
+ /** Force-snapshot cadence inside the window, seconds (predecessor parity). */
31318
+ cadenceSec: CadenceSecField.default(15),
31319
+ /** Output frames per second of the assembled mp4 (predecessor parity). */
31320
+ framerate: FramerateField.default(10),
31321
+ /** `notification-output` targets the finished video/thumbnail is sent to. */
31322
+ targets: TargetsField,
31323
+ template: TimelapseTemplateSchema.optional(),
31324
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
31325
+ priority: PriorityField.default(3)
31326
+ });
31327
+ object({
31328
+ name: NameField.optional(),
31329
+ enabled: boolean().optional(),
31330
+ deviceIds: DeviceIdsField.optional(),
31331
+ schedule: NcScheduleSchema.optional(),
31332
+ cadenceSec: CadenceSecField.optional(),
31333
+ framerate: FramerateField.optional(),
31334
+ targets: TargetsField.optional(),
31335
+ template: TimelapseTemplateSchema.nullable().optional(),
31336
+ priority: PriorityField.optional()
31337
+ });
31338
+ TimelapseRuleInputSchema.extend({
31339
+ id: string(),
31340
+ /**
31341
+ * Ownership/visibility key. Absent = admin/global rule (visible to all).
31342
+ * Present = personal rule owned by this userId. Server-stamped from the
31343
+ * resolved caller; never trusted from a client payload.
31344
+ */
31345
+ ownerUserId: string().optional(),
31346
+ /**
31347
+ * Epoch-ms of the last successful generation — the 1-hour re-generation
31348
+ * guard's durable state (predecessor parity). Absent = never generated.
31349
+ */
31350
+ lastGeneratedAt: number().optional(),
31351
+ /** userId of the caller who created the rule (server-stamped). */
31352
+ createdBy: string(),
31353
+ createdAt: number(),
31354
+ updatedAt: number()
31355
+ });
31429
31356
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
31430
31357
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
31431
31358
  /**
@@ -31744,6 +31671,193 @@ function pickNativeLeaseOverride(view) {
31744
31671
  }
31745
31672
  return readNativeLeaseOverride(flat);
31746
31673
  }
31674
+ function hfModelUrl(repo, path) {
31675
+ return `https://huggingface.co/${repo}/resolve/main/${path}`;
31676
+ }
31677
+ /**
31678
+ * Names that, when used as URL query parameters, almost certainly carry
31679
+ * a secret. Matching is case-insensitive and anchored to the full param
31680
+ * name to avoid false positives like `user_id` (an entity reference, not
31681
+ * a credential) or `topic=password-reset` (a value happens to contain
31682
+ * the substring "password" but the param name is unrelated).
31683
+ *
31684
+ * Add new patterns here as we encounter cameras/APIs that leak through
31685
+ * the existing list.
31686
+ */
31687
+ var CREDENTIAL_PARAM_PATTERNS = [
31688
+ /^user$/i,
31689
+ /^usr$/i,
31690
+ /^username$/i,
31691
+ /^password$/i,
31692
+ /^pwd$/i,
31693
+ /^pass$/i,
31694
+ /^passwd$/i,
31695
+ /^secret$/i,
31696
+ /^token$/i,
31697
+ /^auth$/i,
31698
+ /^auth[_-]?token$/i,
31699
+ /^bearer$/i,
31700
+ /^api[_-]?key$/i,
31701
+ /^access[_-]?token$/i,
31702
+ /^refresh[_-]?token$/i
31703
+ ];
31704
+ function isCredentialParam(name) {
31705
+ return CREDENTIAL_PARAM_PATTERNS.some((re) => re.test(name));
31706
+ }
31707
+ /**
31708
+ * Redact secrets we may surface in logs or telemetry:
31709
+ * 1. `userinfo` (`user:password@host`) — the classic RTSP shape.
31710
+ * 2. Query-string credential params (`?user=...&password=...`) — the
31711
+ * Reolink RTMP shape and several HTTP-pushed-stream APIs.
31712
+ *
31713
+ * Returns the original string verbatim when the input is not a parseable
31714
+ * URL — a log helper must never throw. Param values are replaced with
31715
+ * `***` so an operator reading logs still sees WHICH credential was
31716
+ * present (useful for debugging "wrong password" issues) without
31717
+ * exposing the value.
31718
+ *
31719
+ * The credential-name match is case-insensitive but anchored to the
31720
+ * whole param name; values like `user_id=42` or `topic=password-reset`
31721
+ * are NOT masked (those are entity references, not credentials).
31722
+ */
31723
+ function maskUrlCredentials(rawUrl) {
31724
+ try {
31725
+ const u = new URL(rawUrl);
31726
+ let touched = false;
31727
+ if (u.username || u.password) {
31728
+ u.username = "";
31729
+ u.password = "";
31730
+ touched = true;
31731
+ }
31732
+ for (const key of Array.from(u.searchParams.keys())) if (isCredentialParam(key)) {
31733
+ u.searchParams.set(key, "***");
31734
+ touched = true;
31735
+ }
31736
+ return touched ? u.toString() : rawUrl;
31737
+ } catch {
31738
+ return rawUrl;
31739
+ }
31740
+ }
31741
+ /**
31742
+ * Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
31743
+ * drain() returns up to maxCount items in FIFO order and removes them.
31744
+ */
31745
+ var RingBuffer = class {
31746
+ capacity;
31747
+ items;
31748
+ head = 0;
31749
+ tail = 0;
31750
+ count = 0;
31751
+ constructor(capacity) {
31752
+ this.capacity = capacity;
31753
+ this.items = Array.from({ length: capacity });
31754
+ }
31755
+ get size() {
31756
+ return this.count;
31757
+ }
31758
+ push(item) {
31759
+ this.items[this.tail] = item;
31760
+ this.tail = (this.tail + 1) % this.capacity;
31761
+ if (this.count < this.capacity) this.count++;
31762
+ else this.head = (this.head + 1) % this.capacity;
31763
+ }
31764
+ drain(maxCount) {
31765
+ const result = [];
31766
+ const n = Math.min(maxCount, this.count);
31767
+ for (let i = 0; i < n; i++) {
31768
+ result.push(this.items[this.head]);
31769
+ this.items[this.head] = void 0;
31770
+ this.head = (this.head + 1) % this.capacity;
31771
+ }
31772
+ this.count -= n;
31773
+ return result;
31774
+ }
31775
+ };
31776
+ /**
31777
+ * DEVICE-POOL backend → model format — the SSOT for the multi-device inference
31778
+ * DESCRIPTOR path (Python per-device pools), distinct from {@link BACKEND_TO_FORMAT}
31779
+ * above which is the legacy NODE runtime map (where `coreml → onnx` because
31780
+ * onnxruntime-node's CoreML EP loads `.onnx`). Here a device pool runs the
31781
+ * backend NATIVELY, so `coreml → coreml` and `edgetpu → tflite`. Consumed by
31782
+ * `resolveDeviceEngine` (addon-pipeline) and the orchestrator's
31783
+ * `parseDeviceKey`/`getNodeInferenceDevices` — the previously-duplicated
31784
+ * `BACKEND_FORMAT` maps (R3/node-F2). `cpu`/`cuda` floor to onnx.
31785
+ */
31786
+ var DEVICE_BACKEND_TO_FORMAT = {
31787
+ openvino: "openvino",
31788
+ edgetpu: "tflite",
31789
+ coreml: "coreml",
31790
+ onnx: "onnx",
31791
+ cpu: "onnx",
31792
+ cuda: "onnx"
31793
+ };
31794
+ /**
31795
+ * Evaluate `rules` against `items`. Returns the items partitioned
31796
+ * into `passed` (kept by the gate) and `excluded` (dropped). When
31797
+ * `rules` is empty or `zones` is empty everything passes — the gate
31798
+ * is "off" without explicit configuration.
31799
+ *
31800
+ * `getCenter` returns the item's normalised (0–1) center point.
31801
+ * `getClassName` is optional — when omitted, every rule's
31802
+ * `classFilter` is treated as matching all classes (motion-stage
31803
+ * semantics). Provide it for object detections so per-class rules
31804
+ * narrow correctly.
31805
+ */
31806
+ function evaluateZoneRules(items, zones, rules, getCenter, getClassName) {
31807
+ if (rules.length === 0 || zones.length === 0) return {
31808
+ passed: items,
31809
+ excluded: []
31810
+ };
31811
+ const activeRules = rules.filter((r) => r.enabled !== false);
31812
+ const includeRules = activeRules.filter((r) => r.mode === "include");
31813
+ const excludeRules = activeRules.filter((r) => r.mode === "exclude");
31814
+ const whitelistMode = includeRules.length > 0;
31815
+ const zonesById = new Map(zones.map((z) => [z.id, z]));
31816
+ const passed = [];
31817
+ const excluded = [];
31818
+ for (const item of items) {
31819
+ const center = getCenter(item);
31820
+ const className = getClassName?.(item);
31821
+ const inInclude = includeRules.some((r) => ruleApplies(r, center, className, zonesById));
31822
+ const inExclude = excludeRules.some((r) => ruleApplies(r, center, className, zonesById));
31823
+ if (whitelistMode) if (inInclude && !inExclude) passed.push(item);
31824
+ else excluded.push(item);
31825
+ else if (inExclude) excluded.push(item);
31826
+ else passed.push(item);
31827
+ }
31828
+ return {
31829
+ passed,
31830
+ excluded
31831
+ };
31832
+ }
31833
+ function ruleApplies(rule, center, className, zonesById) {
31834
+ if (rule.classFilter && rule.classFilter.length > 0) {
31835
+ if (className !== void 0 && !rule.classFilter.includes(className)) return false;
31836
+ }
31837
+ for (const zoneId of rule.zoneIds) {
31838
+ const zone = zonesById.get(zoneId);
31839
+ if (!zone) continue;
31840
+ if (pointInPolygon(center, zone.polygon)) return true;
31841
+ }
31842
+ return false;
31843
+ }
31844
+ /**
31845
+ * Standard ray-casting point-in-polygon. Polygon vertices are
31846
+ * normalised (0–1); the test point is too — frame-resolution-
31847
+ * agnostic. Boundary points count as inside (treats `<` as `<=` on
31848
+ * the y-edge crossing).
31849
+ */
31850
+ function pointInPolygon(point, polygon) {
31851
+ if (polygon.length < 3) return false;
31852
+ let inside = false;
31853
+ const { x, y } = point;
31854
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
31855
+ const a = polygon[i];
31856
+ const b = polygon[j];
31857
+ if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y || Number.EPSILON) + a.x) inside = !inside;
31858
+ }
31859
+ return inside;
31860
+ }
31747
31861
  var AUTO = {
31748
31862
  value: "auto",
31749
31863
  label: "Auto"