@camstack/addon-pipeline 1.2.47 → 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-C9Q1t0nT.js → dist-CksKE1Sn.js} +1262 -1283
  6. package/dist/{dist-CfxC7XoI.mjs → dist-DoTw0NM3.mjs} +1262 -1283
  7. package/dist/{event-loop-stall-monitor-B_XUPTwp.mjs → event-loop-stall-monitor-CtktCf3u.mjs} +1 -1
  8. package/dist/{event-loop-stall-monitor-Luq9AX4t.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-SKj0Zdz2.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-D8gyxL0j.mjs → hostInit-DqD1qq8w.mjs} +2 -2
  22. package/dist/stream-broker/index.js +196 -130
  23. package/dist/stream-broker/index.mjs +196 -130
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/{worker-protocol-rqbyKlm9.mjs → worker-protocol-4_4L1QOe.mjs} +1 -1
  26. package/dist/{worker-protocol-BUJpm7l3.js → worker-protocol-CuEcTi66.js} +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DwXIyaHw.js → MaskShapeCanvas-DI4BY7W2-EcWZ5m8e.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Cf8EtWEG.js → MotionZonesSettings-NcxxQN8r-CfiyCD3z.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BvsVXya9.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-KpixNEma.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-rd7UaqfV.mjs +0 -26
  35. package/embed-dist/assets/index-BuAeIGv8.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
  /**
@@ -10542,12 +10542,34 @@ var streamBrokerCapability = {
10542
10542
  kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
10543
10543
  /** GIF geometry. The video keeps the source's own. */
10544
10544
  gifMaxWidth: number().int().min(120).max(1280).default(640),
10545
- gifFps: number().int().min(1).max(15).default(8),
10546
10545
  /**
10547
- * Playback rate, applied to EVERY container so they stay one clip.
10548
- * `1` is real time and is what allows the copy branch.
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.
10549
10571
  */
10550
- speed: number().min(1).max(8).default(1)
10572
+ speed: number().min(1).max(8).default(4)
10551
10573
  }), EventMediaProductionSchema, {
10552
10574
  kind: "mutation",
10553
10575
  auth: "admin"
@@ -10998,25 +11020,6 @@ var cameraStreamsCapability = {
10998
11020
  lastChangedAt: number()
10999
11021
  })
11000
11022
  };
11001
- /**
11002
- * core-blocks — user-authored TypeScript, stored in the kernel and executed in
11003
- * its own process.
11004
- *
11005
- * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
11006
- *
11007
- * The first use is **owning devices without being a device provider**: a block
11008
- * declares devices under a system or custom integration and drives their state,
11009
- * with the same `ctx` an addon gets. Automations come later; nothing here
11010
- * models a trigger.
11011
- *
11012
- * **Stated plainly, because it does not change by being true:** a block has an
11013
- * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
11014
- * with no review step. What makes that survivable is not a sandbox, it is
11015
- * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
11016
- * so a block that throws or never returns is marked `failed` and visible
11017
- * instead of taking the hub with it (D6). Every method here is admin-only, and
11018
- * must stay so.
11019
- */
11020
11023
  /** Where a block runs. The operator chooses — a block driving a device on an
11021
11024
  * agent is the reason placement is not fixed to the hub. */
11022
11025
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
@@ -11088,6 +11091,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
11088
11091
  }), object({ block: CoreBlockSchema }), {
11089
11092
  kind: "mutation",
11090
11093
  auth: "admin"
11094
+ }), method(object({ blockId: string() }), object({ block: CoreBlockSchema }), {
11095
+ kind: "mutation",
11096
+ auth: "admin"
11091
11097
  }), method(object({ code: string() }), CoreBlockCompileResultSchema, {
11092
11098
  kind: "mutation",
11093
11099
  auth: "admin"
@@ -11819,713 +11825,118 @@ var ExposeInputSchema = object({
11819
11825
  });
11820
11826
  var UnexposeInputSchema = object({ deviceId: string() });
11821
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
+ });
11822
11839
  /**
11823
- * Resource-bound constants for the safe expression engine.
11824
- *
11825
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
11826
- * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
11827
- * O(nodeCount) by construction. These caps merely put a hard ceiling on the
11828
- * work a single author-supplied expression can request, so a hostile or
11829
- * 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`.
11830
11843
  */
11831
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
11832
- * rejected without allocation. */
11833
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
11834
- /** A legal binding / identifier name. */
11835
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
11836
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
11837
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
11838
- var RESERVED_BINDING_NAMES = new Set([
11839
- "now",
11840
- "true",
11841
- "false",
11842
- "null"
11843
- ]);
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
+ });
11844
11857
  /**
11845
- * Error types for the safe expression engine. Two distinct classes so callers
11846
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
11847
- * 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.
11848
11861
  */
11849
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
11850
- * the failure is anchored to a character (author-facing inline feedback). */
11851
- var ExpressionParseError = class extends Error {
11852
- position;
11853
- constructor(message, position) {
11854
- super(message);
11855
- this.name = "ExpressionParseError";
11856
- this.position = position;
11857
- }
11858
- };
11859
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
11860
- * result, unknown builtin, step-budget exceeded). */
11861
- var ExpressionEvalError = class extends Error {
11862
- constructor(message) {
11863
- super(message);
11864
- this.name = "ExpressionEvalError";
11865
- }
11866
- };
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
+ });
11867
11878
  /**
11868
- * Frozen, null-prototype builtin function table for the expression engine
11869
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
11870
- * parser rejects any callee not in it, and the evaluator gates each call on an
11871
- * own-property check against it.
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.
11883
+ */
11884
+ var FieldProbeResultSchema = object({
11885
+ status: _enum(["ok", "error"]),
11886
+ labels: array(string()).optional(),
11887
+ error: string().optional()
11888
+ });
11889
+ /**
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.
11896
+ */
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
+ });
11923
+ /**
11924
+ * Device Manager capability — hub-side singleton that unifies device persistence,
11925
+ * live registry access, and all management operations into a single tRPC surface.
11872
11926
  *
11873
- * Because the object has a NULL prototype AND is `Object.freeze`d:
11874
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
11875
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
11876
- * (there is no `Object.prototype` in the chain), so those names are not
11877
- * callable — they are simply "unknown function" at parse time.
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)
11878
11931
  *
11879
- * Every numeric argument is validated as a finite number and every numeric
11880
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
11881
- * (`pow(10,400)` Infinity) all raise `ExpressionEvalError` and fail the link
11882
- * closed rather than emitting a garbage value.
11883
- */
11884
- function asFiniteNumber(value, name, index) {
11885
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
11886
- return value;
11887
- }
11888
- function asString$1(value, name, index) {
11889
- if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
11890
- return value;
11891
- }
11892
- function finiteResult(value, name) {
11893
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
11894
- return value;
11895
- }
11896
- function allFiniteNumbers(args, name) {
11897
- return args.map((a, idx) => asFiniteNumber(a, name, idx));
11898
- }
11899
- var INF = Number.POSITIVE_INFINITY;
11900
- var table = {
11901
- min: {
11902
- minArgs: 1,
11903
- maxArgs: INF,
11904
- apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
11905
- },
11906
- max: {
11907
- minArgs: 1,
11908
- maxArgs: INF,
11909
- apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
11910
- },
11911
- abs: {
11912
- minArgs: 1,
11913
- maxArgs: 1,
11914
- apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
11915
- },
11916
- floor: {
11917
- minArgs: 1,
11918
- maxArgs: 1,
11919
- apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
11920
- },
11921
- ceil: {
11922
- minArgs: 1,
11923
- maxArgs: 1,
11924
- apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
11925
- },
11926
- sqrt: {
11927
- minArgs: 1,
11928
- maxArgs: 1,
11929
- apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
11930
- },
11931
- round: {
11932
- minArgs: 1,
11933
- maxArgs: 2,
11934
- apply: (args) => {
11935
- const x = asFiniteNumber(args[0], "round", 0);
11936
- const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
11937
- if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
11938
- const factor = 10 ** digits;
11939
- return finiteResult(Math.round(x * factor) / factor, "round");
11940
- }
11941
- },
11942
- pow: {
11943
- minArgs: 2,
11944
- maxArgs: 2,
11945
- apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
11946
- },
11947
- clamp: {
11948
- minArgs: 3,
11949
- maxArgs: 3,
11950
- apply: (args) => {
11951
- const x = asFiniteNumber(args[0], "clamp", 0);
11952
- const lo = asFiniteNumber(args[1], "clamp", 1);
11953
- const hi = asFiniteNumber(args[2], "clamp", 2);
11954
- if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
11955
- return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
11956
- }
11957
- },
11958
- avg: {
11959
- minArgs: 1,
11960
- maxArgs: INF,
11961
- apply: (args) => {
11962
- const nums = allFiniteNumbers(args, "avg");
11963
- return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
11964
- }
11965
- },
11966
- sum: {
11967
- minArgs: 1,
11968
- maxArgs: INF,
11969
- apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
11970
- },
11971
- coalesce: {
11972
- minArgs: 1,
11973
- maxArgs: INF,
11974
- apply: (args) => {
11975
- for (const a of args) if (a !== null) return a;
11976
- return null;
11977
- }
11978
- },
11979
- age: {
11980
- minArgs: 2,
11981
- maxArgs: 2,
11982
- apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
11983
- },
11984
- convert: {
11985
- minArgs: 3,
11986
- maxArgs: 3,
11987
- apply: (args, hooks) => {
11988
- const x = asFiniteNumber(args[0], "convert", 0);
11989
- const from = asString$1(args[1], "convert", 1).trim();
11990
- const to = asString$1(args[2], "convert", 2).trim();
11991
- if (hooks.convert) {
11992
- const out = hooks.convert(x, from, to);
11993
- if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
11994
- return finiteResult(out, "convert");
11995
- }
11996
- if (from === to) return x;
11997
- throw new ExpressionEvalError("convert: unit conversion table not installed");
11998
- }
11999
- }
12000
- };
12001
- Object.freeze(Object.assign(Object.create(null), table));
12002
- /** The set of valid builtin names — used by the parser to reject unknown
12003
- * callees at parse time (immediate author feedback). */
12004
- var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
12005
- /**
12006
- * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
12007
- * zero-dependency. The grammar is deliberately boring: decimal numbers,
12008
- * single/double-quoted strings with a tiny escape set, identifiers, the three
12009
- * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
12010
- * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
12011
- * is a parse error with a source position, so member access / assignment /
12012
- * template literals are lexically impossible.
12013
- */
12014
- var KEYWORDS = new Set([
12015
- "true",
12016
- "false",
12017
- "null"
12018
- ]);
12019
- function isDigit(ch) {
12020
- return ch >= "0" && ch <= "9";
12021
- }
12022
- function isIdentStart(ch) {
12023
- return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
12024
- }
12025
- function isIdentPart(ch) {
12026
- return isIdentStart(ch) || isDigit(ch);
12027
- }
12028
- function isWhitespace(ch) {
12029
- return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
12030
- }
12031
- /** Tokenize `source` into a flat token list ending with a single `eof` token.
12032
- * Throws `ExpressionParseError` on any illegal character or unterminated
12033
- * string. */
12034
- function tokenize(source) {
12035
- if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
12036
- const tokens = [];
12037
- let i = 0;
12038
- const n = source.length;
12039
- while (i < n) {
12040
- const ch = source[i];
12041
- if (isWhitespace(ch)) {
12042
- i += 1;
12043
- continue;
12044
- }
12045
- if (isDigit(ch)) {
12046
- const start = i;
12047
- while (i < n && isDigit(source[i])) i += 1;
12048
- if (i < n && source[i] === ".") {
12049
- if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
12050
- i += 1;
12051
- while (i < n && isDigit(source[i])) i += 1;
12052
- }
12053
- const text = source.slice(start, i);
12054
- const value = Number(text);
12055
- if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
12056
- tokens.push({
12057
- type: "number",
12058
- value,
12059
- pos: start
12060
- });
12061
- continue;
12062
- }
12063
- if (ch === "'" || ch === "\"") {
12064
- const quote = ch;
12065
- const start = i;
12066
- i += 1;
12067
- let out = "";
12068
- let closed = false;
12069
- while (i < n) {
12070
- const c = source[i];
12071
- if (c === "\\") {
12072
- const next = i + 1 < n ? source[i + 1] : "";
12073
- if (next === "\\" || next === "'" || next === "\"") {
12074
- out += next;
12075
- i += 2;
12076
- continue;
12077
- }
12078
- throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
12079
- }
12080
- if (c === quote) {
12081
- closed = true;
12082
- i += 1;
12083
- break;
12084
- }
12085
- out += c;
12086
- i += 1;
12087
- }
12088
- if (!closed) throw new ExpressionParseError("unterminated string literal", start);
12089
- tokens.push({
12090
- type: "string",
12091
- value: out,
12092
- pos: start
12093
- });
12094
- continue;
12095
- }
12096
- if (isIdentStart(ch)) {
12097
- const start = i;
12098
- while (i < n && isIdentPart(source[i])) i += 1;
12099
- const text = source.slice(start, i);
12100
- if (KEYWORDS.has(text)) tokens.push({
12101
- type: "keyword",
12102
- keyword: keywordOf(text),
12103
- pos: start
12104
- });
12105
- else tokens.push({
12106
- type: "identifier",
12107
- name: text,
12108
- pos: start
12109
- });
12110
- continue;
12111
- }
12112
- const two = i + 1 < n ? source.slice(i, i + 2) : "";
12113
- if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
12114
- tokens.push({
12115
- type: "punct",
12116
- punct: two,
12117
- pos: i
12118
- });
12119
- i += 2;
12120
- continue;
12121
- }
12122
- if (isSinglePunct(ch)) {
12123
- tokens.push({
12124
- type: "punct",
12125
- punct: ch,
12126
- pos: i
12127
- });
12128
- i += 1;
12129
- continue;
12130
- }
12131
- throw new ExpressionParseError(`unexpected character '${ch}'`, i);
12132
- }
12133
- tokens.push({
12134
- type: "eof",
12135
- pos: n
12136
- });
12137
- return tokens;
12138
- }
12139
- function keywordOf(text) {
12140
- if (text === "true") return "true";
12141
- if (text === "false") return "false";
12142
- return "null";
12143
- }
12144
- function isSinglePunct(ch) {
12145
- return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
12146
- }
12147
- /**
12148
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
12149
- *
12150
- * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
12151
- * → relational → additive → multiplicative → unary `! -` → call / primary.
12152
- * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
12153
- * string validated against the builtin table at parse time, so an unknown
12154
- * function is rejected immediately (author feedback) and a persisted expression
12155
- * that references a since-removed builtin degrades at read.
12156
- *
12157
- * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
12158
- * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
12159
- */
12160
- /** Binary/logical operator precedence (higher binds tighter). */
12161
- var BINARY_PRECEDENCE = {
12162
- "||": 1,
12163
- "&&": 2,
12164
- "==": 3,
12165
- "!=": 3,
12166
- "<": 4,
12167
- "<=": 4,
12168
- ">": 4,
12169
- ">=": 4,
12170
- "+": 5,
12171
- "-": 5,
12172
- "*": 6,
12173
- "/": 6,
12174
- "%": 6
12175
- };
12176
- function isLogicalOp(op) {
12177
- return op === "&&" || op === "||";
12178
- }
12179
- function isBinaryOp(op) {
12180
- return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
12181
- }
12182
- var Parser = class {
12183
- tokens;
12184
- pos = 0;
12185
- nodeCount = 0;
12186
- identifiers = /* @__PURE__ */ new Set();
12187
- callees = /* @__PURE__ */ new Set();
12188
- constructor(tokens) {
12189
- this.tokens = tokens;
12190
- }
12191
- parse() {
12192
- const ast = this.parseTernary();
12193
- const tok = this.peek();
12194
- if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
12195
- return {
12196
- ast,
12197
- identifiers: this.identifiers,
12198
- callees: this.callees,
12199
- nodeCount: this.nodeCount
12200
- };
12201
- }
12202
- peek() {
12203
- return this.tokens[this.pos];
12204
- }
12205
- next() {
12206
- return this.tokens[this.pos++];
12207
- }
12208
- /** Consume a punctuator token, erroring if the next token isn't it. */
12209
- expectPunct(punct) {
12210
- const tok = this.peek();
12211
- if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
12212
- this.pos += 1;
12213
- }
12214
- matchPunct(punct) {
12215
- const tok = this.peek();
12216
- if (tok.type === "punct" && tok.punct === punct) {
12217
- this.pos += 1;
12218
- return true;
12219
- }
12220
- return false;
12221
- }
12222
- countNode() {
12223
- this.nodeCount += 1;
12224
- if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
12225
- }
12226
- parseTernary() {
12227
- const test = this.parseBinary(1);
12228
- if (this.matchPunct("?")) {
12229
- const consequent = this.parseTernary();
12230
- this.expectPunct(":");
12231
- const alternate = this.parseTernary();
12232
- this.countNode();
12233
- return {
12234
- kind: "conditional",
12235
- test,
12236
- consequent,
12237
- alternate
12238
- };
12239
- }
12240
- return test;
12241
- }
12242
- parseBinary(minPrec) {
12243
- let left = this.parseUnary();
12244
- for (;;) {
12245
- const tok = this.peek();
12246
- if (tok.type !== "punct") break;
12247
- const prec = BINARY_PRECEDENCE[tok.punct];
12248
- if (prec === void 0 || prec < minPrec) break;
12249
- const op = tok.punct;
12250
- this.pos += 1;
12251
- const right = this.parseBinary(prec + 1);
12252
- this.countNode();
12253
- if (isLogicalOp(op)) left = {
12254
- kind: "logical",
12255
- op,
12256
- left,
12257
- right
12258
- };
12259
- else if (isBinaryOp(op)) left = {
12260
- kind: "binary",
12261
- op,
12262
- left,
12263
- right
12264
- };
12265
- else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
12266
- }
12267
- return left;
12268
- }
12269
- parseUnary() {
12270
- const tok = this.peek();
12271
- if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
12272
- const op = tok.punct;
12273
- this.pos += 1;
12274
- const operand = this.parseUnary();
12275
- this.countNode();
12276
- return {
12277
- kind: "unary",
12278
- op,
12279
- operand
12280
- };
12281
- }
12282
- return this.parsePrimary();
12283
- }
12284
- parsePrimary() {
12285
- const tok = this.next();
12286
- switch (tok.type) {
12287
- case "number":
12288
- this.countNode();
12289
- return {
12290
- kind: "literal",
12291
- value: tok.value
12292
- };
12293
- case "string":
12294
- this.countNode();
12295
- return {
12296
- kind: "literal",
12297
- value: tok.value
12298
- };
12299
- case "keyword":
12300
- this.countNode();
12301
- return {
12302
- kind: "literal",
12303
- value: tok.keyword === "null" ? null : tok.keyword === "true"
12304
- };
12305
- case "identifier": {
12306
- const nextTok = this.peek();
12307
- if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
12308
- this.identifiers.add(tok.name);
12309
- this.countNode();
12310
- return {
12311
- kind: "identifier",
12312
- name: tok.name
12313
- };
12314
- }
12315
- case "punct":
12316
- if (tok.punct === "(") {
12317
- const inner = this.parseTernary();
12318
- this.expectPunct(")");
12319
- return inner;
12320
- }
12321
- throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
12322
- case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
12323
- }
12324
- }
12325
- parseCall(callee, pos) {
12326
- if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
12327
- this.expectPunct("(");
12328
- const args = [];
12329
- if (!this.matchPunct(")")) for (;;) {
12330
- args.push(this.parseTernary());
12331
- if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
12332
- if (this.matchPunct(",")) continue;
12333
- this.expectPunct(")");
12334
- break;
12335
- }
12336
- this.callees.add(callee);
12337
- this.countNode();
12338
- return {
12339
- kind: "call",
12340
- callee,
12341
- args
12342
- };
12343
- }
12344
- };
12345
- /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
12346
- * `ExpressionParseError` on any lexical or grammatical failure. */
12347
- function parseExpression(source) {
12348
- return new Parser(tokenize(source)).parse();
12349
- }
12350
- /**
12351
- * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
12352
- * by expr"). The cache stores BOTH successes and failures (negative caching),
12353
- * so a corrupt persisted string costs exactly one tokenize+parse total — not
12354
- * one per read on a hot resolve path.
12355
- *
12356
- * The cache is a module-level singleton: entries are pure, content-addressed
12357
- * ASTs keyed by the raw source string, so sharing one instance across all
12358
- * callers is safe and maximises hit rate.
12359
- */
12360
- var cache = /* @__PURE__ */ new Map();
12361
- function getCached(source) {
12362
- const hit = cache.get(source);
12363
- if (hit !== void 0) {
12364
- cache.delete(source);
12365
- cache.set(source, hit);
12366
- return hit;
12367
- }
12368
- let result;
12369
- try {
12370
- result = {
12371
- ok: true,
12372
- parsed: parseExpression(source)
12373
- };
12374
- } catch (err) {
12375
- result = {
12376
- ok: false,
12377
- error: err instanceof ExpressionParseError ? err.message : String(err)
12378
- };
12379
- }
12380
- cache.set(source, result);
12381
- if (cache.size > 256) {
12382
- const oldest = cache.keys().next().value;
12383
- if (oldest !== void 0) cache.delete(oldest);
12384
- }
12385
- return result;
12386
- }
12387
- /** Compile `source`, returning a discriminated result instead of throwing.
12388
- * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
12389
- function compileExpressionSafe(source) {
12390
- return getCached(source);
12391
- }
12392
- Object.freeze({});
12393
- /**
12394
- * Author-time validation. Returns `null` when the source is valid, else a
12395
- * human-readable error message. Checks: the expression compiles; binding count
12396
- * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
12397
- * is not reserved (`now`/keywords) and does not shadow a builtin; and every
12398
- * FREE identifier of the AST is covered by a binding or the injected `now`.
12399
- */
12400
- function validateExpressionSource(src) {
12401
- const names = Object.keys(src.bindings);
12402
- if (names.length > 32) return `too many bindings (${names.length} > 32)`;
12403
- for (const name of names) {
12404
- if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
12405
- if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
12406
- if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
12407
- }
12408
- const compiled = compileExpressionSafe(src.expr);
12409
- if (!compiled.ok) return compiled.error;
12410
- const bound = new Set(names);
12411
- for (const id of compiled.parsed.identifiers) {
12412
- if (id === "now") continue;
12413
- if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
12414
- }
12415
- return null;
12416
- }
12417
- var ProviderStatusSchema = object({
12418
- connected: boolean(),
12419
- deviceCount: number(),
12420
- error: string().optional()
12421
- });
12422
- object({
12423
- externalId: string(),
12424
- name: string(),
12425
- type: string(),
12426
- metadata: record(string(), unknown()).optional()
12427
- });
12428
- /**
12429
- * Candidate handed back from discovery and accepted by
12430
- * `adoptDiscoveredDevice`. Shape mirrors the in-process
12431
- * `DiscoveredDevice` interface used by `DeviceDiscovery`.
12432
- */
12433
- var DiscoveryCandidateSchema = object({
12434
- stableId: string(),
12435
- type: _enum(DeviceType),
12436
- suggestedName: string(),
12437
- prefilledConfig: record(string(), unknown()),
12438
- /**
12439
- * Optional upstream-system identity (HA entity_id, vendor MAC, …).
12440
- * Discovery pre-populates this for systems that know the upstream
12441
- * identity ahead of adoption. Rendering metadata (unit, precision)
12442
- * flows live through the cap STATUS SLICE after adoption.
12443
- */
12444
- sourceInfo: SourceInfoSchema.optional()
12445
- });
12446
- /**
12447
- * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
12448
- * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
12449
- * tRPC layer can pass it through without reshaping.
12450
- */
12451
- var DeviceSummarySchema = object({
12452
- id: number(),
12453
- stableId: string(),
12454
- addonId: string(),
12455
- type: string(),
12456
- name: string(),
12457
- parentDeviceId: number().nullable(),
12458
- online: boolean(),
12459
- features: array(string()),
12460
- config: record(string(), unknown()),
12461
- /** Optional upstream-system identity (dispatch key + system tag).
12462
- * See `SourceInfo`. Present when the device has a non-synthetic
12463
- * source identifier (HA entities, vendor MAC, …); omitted when the
12464
- * synthetic backfill is in effect. */
12465
- sourceInfo: SourceInfoSchema.optional()
12466
- });
12467
- /**
12468
- * Result of a live field test (e.g. probing an RTSP URL during device
12469
- * creation). Matches the UI-side `FieldProbeResult` in
12470
- * `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
12471
- * returned `labels` as chips next to the input.
12472
- */
12473
- var FieldProbeResultSchema = object({
12474
- status: _enum(["ok", "error"]),
12475
- labels: array(string()).optional(),
12476
- error: string().optional()
12477
- });
12478
- /**
12479
- * The output of `getChildCreationSchema` is a UI schema tree. We store
12480
- * it as `unknown` at the capability layer — the router just passes it
12481
- * through and the admin UI renders it via `FormBuilder`. The actual
12482
- * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
12483
- * but we deliberately avoid a Zod mirror because the union is large and
12484
- * not meant for runtime validation at this seam.
12485
- */
12486
- var CreationSchemaOutputSchema = unknown();
12487
- method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
12488
- id: string(),
12489
- name: string(),
12490
- type: string()
12491
- }))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
12492
- kind: "mutation",
12493
- auth: "admin"
12494
- }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
12495
- kind: "mutation",
12496
- auth: "admin"
12497
- }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
12498
- type: _enum(DeviceType),
12499
- config: record(string(), unknown())
12500
- }), DeviceSummarySchema, {
12501
- kind: "mutation",
12502
- auth: "admin"
12503
- }), method(object({
12504
- type: _enum(DeviceType),
12505
- key: string(),
12506
- value: unknown(),
12507
- formValues: record(string(), unknown()).optional()
12508
- }), FieldProbeResultSchema, {
12509
- kind: "mutation",
12510
- auth: "admin"
12511
- });
12512
- /**
12513
- * Device Manager capability — hub-side singleton that unifies device persistence,
12514
- * live registry access, and all management operations into a single tRPC surface.
12515
- *
12516
- * Replaces:
12517
- * - `device-persistence` capability (persistence methods absorbed here)
12518
- * - `device-management.router.ts` (deleted in Phase 2)
12519
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
12520
- *
12521
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
12522
- * fork into separate processes but never run on remote cluster agents. Therefore:
12523
- * - No nodeId routing needed — this is a pure hub singleton.
12524
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
12525
- * - No shadow registry or cross-node aggregation required.
12526
- *
12527
- * Forked workers register devices back to the hub via `ctx.devices`
12528
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
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.
12529
11940
  */
12530
11941
  /** One child-placement directive on a container's `childLayout`. Structurally
12531
11942
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
@@ -12538,92 +11949,6 @@ var ChildLayoutEntrySchema = object({
12538
11949
  order: number().optional(),
12539
11950
  collapsed: boolean().optional()
12540
11951
  });
12541
- /** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
12542
- * `device-management.ts`. Source is a union: a FIELD source copies a sibling
12543
- * accessory's status field (`kind` optional/absent for wire compat); a
12544
- * LITERAL source carries a per-device constant (no sibling is read); a
12545
- * GLOBAL source (P2e) copies ANY device's status field, addressed by the
12546
- * source device's full re-sync-stable `stableId`. */
12547
- var DeviceLinkFieldSourceSchema = object({
12548
- kind: literal("field").optional(),
12549
- sourceKey: string(),
12550
- cap: string(),
12551
- fieldPath: string()
12552
- });
12553
- var DeviceLinkLiteralSourceSchema = object({
12554
- kind: literal("literal"),
12555
- value: union([
12556
- string(),
12557
- number(),
12558
- boolean(),
12559
- _null()
12560
- ])
12561
- });
12562
- var DeviceLinkGlobalSourceSchema = object({
12563
- kind: literal("global"),
12564
- sourceStableId: string(),
12565
- cap: string(),
12566
- fieldPath: string()
12567
- });
12568
- /** Expression source (Stage X): compute the target field from N named bindings
12569
- * via the safe expression engine. Bindings are field | literal | global — never
12570
- * another expression (no nesting). The `superRefine` runs the SAME author-time
12571
- * validation as `validateExpressionSource` (compiles the expr, checks binding
12572
- * names + identifier coverage) so every wire boundary that parses a DeviceLink
12573
- * (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
12574
- * Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
12575
- var DeviceLinkExpressionSourceSchema = object({
12576
- kind: literal("expression"),
12577
- expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
12578
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
12579
- DeviceLinkFieldSourceSchema,
12580
- DeviceLinkLiteralSourceSchema,
12581
- DeviceLinkGlobalSourceSchema
12582
- ]))
12583
- }).superRefine((src, ctx) => {
12584
- const err = validateExpressionSource(src);
12585
- if (err !== null) ctx.addIssue({
12586
- code: "custom",
12587
- message: err,
12588
- path: ["expr"]
12589
- });
12590
- });
12591
- var DeviceLinkSchema = object({
12592
- id: string(),
12593
- source: union([
12594
- DeviceLinkFieldSourceSchema,
12595
- DeviceLinkLiteralSourceSchema,
12596
- DeviceLinkGlobalSourceSchema,
12597
- DeviceLinkExpressionSourceSchema
12598
- ]),
12599
- target: object({
12600
- cap: string(),
12601
- fieldPath: string(),
12602
- itemKey: string().optional()
12603
- }),
12604
- transform: discriminatedUnion("kind", [
12605
- object({ kind: literal("identity") }),
12606
- object({
12607
- kind: literal("enum-map"),
12608
- mapping: record(string(), union([
12609
- string(),
12610
- number(),
12611
- boolean()
12612
- ])),
12613
- fallback: union([
12614
- string(),
12615
- number(),
12616
- boolean()
12617
- ]).optional()
12618
- }),
12619
- object({
12620
- kind: literal("linear"),
12621
- scale: number(),
12622
- offset: number(),
12623
- clamp: tuple([number(), number()]).readonly().optional()
12624
- })
12625
- ]).optional()
12626
- });
12627
11952
  /** Cap-wire shape of a per-cap display refinement — mirrors
12628
11953
  * `DeviceCapDisplayOverride` in `device-management.ts`. */
12629
11954
  var DeviceCapDisplayOverrideSchema = object({
@@ -12703,8 +12028,6 @@ var DeviceInfoSchema = object({
12703
12028
  * named accordion sections (with optional intra-section order). See
12704
12029
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
12705
12030
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
12706
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
12707
- deviceLinks: array(DeviceLinkSchema).readonly().optional(),
12708
12031
  /** Operator-authored per-device display override. See `DeviceMeta.display`. */
12709
12032
  display: DeviceDisplayOverrideSchema.optional()
12710
12033
  });
@@ -12713,7 +12036,7 @@ var ConfigEntrySchema = object({
12713
12036
  value: unknown(),
12714
12037
  description: string().optional()
12715
12038
  });
12716
- var DeviceLinkModeSchema = _enum(["auto", "manual"]);
12039
+ var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
12717
12040
  /** One resolved linked device — the compact projection consumers need. */
12718
12041
  var LinkedDeviceSchema = object({
12719
12042
  deviceId: number(),
@@ -12776,8 +12099,6 @@ var DeviceMetaSchema = object({
12776
12099
  * accordion sections (with optional intra-section order). See
12777
12100
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
12778
12101
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
12779
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
12780
- deviceLinks: array(DeviceLinkSchema).readonly().optional(),
12781
12102
  /** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
12782
12103
  * Optional: only present for accessory children that carry a known role. */
12783
12104
  role: string().nullable().optional(),
@@ -12870,12 +12191,6 @@ method(object({
12870
12191
  }), _void(), {
12871
12192
  kind: "mutation",
12872
12193
  auth: "admin"
12873
- }), method(object({
12874
- deviceId: number(),
12875
- deviceLinks: array(DeviceLinkSchema).readonly()
12876
- }), _void(), {
12877
- kind: "mutation",
12878
- auth: "admin"
12879
12194
  }), method(object({
12880
12195
  deviceId: number(),
12881
12196
  display: DeviceDisplayOverrideSchema.nullable()
@@ -12957,7 +12272,7 @@ method(object({
12957
12272
  * shipping 293 rows to find 12. */
12958
12273
  isCamera: boolean().optional()
12959
12274
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12960
- mode: DeviceLinkModeSchema,
12275
+ mode: LinkedDevicesModeSchema,
12961
12276
  devices: array(LinkedDeviceSchema)
12962
12277
  })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12963
12278
  deviceId: number(),
@@ -12990,11 +12305,7 @@ method(object({
12990
12305
  deviceId: number(),
12991
12306
  entries: array(object({
12992
12307
  capName: string(),
12993
- kind: _enum([
12994
- "native",
12995
- "wrapped",
12996
- "linked"
12997
- ]),
12308
+ kind: _enum(["native", "wrapped"]),
12998
12309
  providerAddonId: string(),
12999
12310
  providerNodeId: string(),
13000
12311
  nativeAddonId: string()
@@ -13003,11 +12314,7 @@ method(object({
13003
12314
  deviceId: number(),
13004
12315
  entries: array(object({
13005
12316
  capName: string(),
13006
- kind: _enum([
13007
- "native",
13008
- "wrapped",
13009
- "linked"
13010
- ]),
12317
+ kind: _enum(["native", "wrapped"]),
13011
12318
  providerAddonId: string(),
13012
12319
  providerNodeId: string(),
13013
12320
  nativeAddonId: string()
@@ -13816,6 +13123,10 @@ var motionDetectionCapability = {
13816
13123
  mode: "singleton",
13817
13124
  kind: "wrapper",
13818
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],
13819
13130
  exposesDeviceSettings: true,
13820
13131
  methods: {
13821
13132
  analyze: method(object({
@@ -16338,6 +15649,18 @@ var OauthIntegrationDescriptorSchema = object({
16338
15649
  * redirect_uri that does not start with one of these. Required —
16339
15650
  * an empty list means the integration can never complete linking. */
16340
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(),
16341
15664
  /** Optional public origin (no trailing slash) that this integration's
16342
15665
  * issued codes/tokens should carry as the `hubUrl` claim — typically the
16343
15666
  * operator-selected external-access endpoint resolved by the addon. When
@@ -16488,7 +15811,7 @@ var TrackEnvelopeSchema = object({
16488
15811
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
16489
15812
  * keeps every scalar the list surfaces actually render (ids, class(es),
16490
15813
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
16491
- * zonesVisited, bestEventId, envelope) and returns `positions` /
15814
+ * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
16492
15815
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
16493
15816
  * `getTrack`. Mirrors the event-store `projection` convention
16494
15817
  * (`getObjectEvents` et al.).
@@ -16725,6 +16048,24 @@ var TrackSchema = object({
16725
16048
  * Populated from the persisted envelope columns on historical reads;
16726
16049
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
16727
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(),
16728
16069
  ...TrackFlagFields,
16729
16070
  ...TrackRetrainFields
16730
16071
  });
@@ -19633,6 +18974,10 @@ var SsoBridgeClaimsSchema = object({
19633
18974
  integrationId: string().optional(),
19634
18975
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
19635
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(),
19636
18981
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
19637
18982
  * tokens so the verify path can check the session is not revoked. */
19638
18983
  sessionId: string().optional()
@@ -20200,7 +19545,7 @@ var ClipPlaybackSchema = object({
20200
19545
  playbackEndpoints: array(string()).optional(),
20201
19546
  token: string().optional()
20202
19547
  });
20203
- method(object({
19548
+ DeviceType.Camera, method(object({
20204
19549
  deviceId: number(),
20205
19550
  since: number(),
20206
19551
  until: number(),
@@ -25456,13 +24801,18 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
25456
24801
  username: string(),
25457
24802
  scopes: array(TokenScopeSchema),
25458
24803
  redirectUri: string(),
25459
- 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()
25460
24808
  }), object({ code: string() }), {
25461
24809
  kind: "mutation",
25462
24810
  access: "create"
25463
24811
  }), method(object({
25464
24812
  code: string(),
25465
- redirectUri: string()
24813
+ redirectUri: string(),
24814
+ /** PKCE verifier. REQUIRED when the code carries a challenge. */
24815
+ codeVerifier: string().optional()
25466
24816
  }), object({
25467
24817
  accessToken: string(),
25468
24818
  refreshToken: string(),
@@ -25689,190 +25039,819 @@ object({
25689
25039
  });
25690
25040
  DeviceType.Weather;
25691
25041
  /**
25692
- * Per-zone occupancy aggregation produced by the analytics frame
25693
- * processor on every inference result. Covers the full combinatorial
25694
- * matrix the operator UI needs: total objects everywhere, total
25695
- * objects per zone, single class everywhere, single class per zone,
25696
- * objects outside any zone.
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.
25697
25254
  *
25698
- * Counts are derived from the analytics tracker (tracked detections
25699
- * with stable trackIds), not raw detector hits this filters out
25700
- * one-off detector flickers and gives counts that match what the user
25701
- * sees on the live overlay.
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.
25702
25260
  *
25703
- * Overlap policy: a detection that intersects two zones counts in
25704
- * BOTH zones' `byClass` and `totalObjects` (count-in-each). The
25705
- * `frame` aggregate de-duplicates trivially since it's frame-wide.
25706
- * `unzoned` counts only detections that landed in zero zones.
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.
25707
25265
  */
25708
- var PerScopeBreakdownSchema = object({
25709
- /** Total tracked objects in this scope (frame / zone / unzoned). */
25710
- totalObjects: number().int().nonnegative(),
25711
- /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
25712
- byClass: record(string(), number().int().nonnegative())
25713
- });
25714
- var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
25715
- zoneId: string(),
25716
- zoneName: string(),
25717
- /** TrackIds of objects currently inside this zone — for cross-reference
25718
- * with the per-track detail panel and live overlay. */
25719
- trackIds: array(string()).readonly()
25720
- });
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));
25721
25387
  /**
25722
- * A parked ("stationary") object surfaced alongside occupancy — an object that
25723
- * settled and stopped moving. It is NO LONGER a tracked object (the tracker was
25724
- * told to forget it so it stops re-spawning tracks/events), but it IS still
25725
- * physically present, so it keeps counting toward `frame` occupancy and is
25726
- * listed here so the UI can show it in a dedicated "Stationary" section instead
25727
- * of flooding the live event feed.
25728
- */
25729
- var StationaryObjectSchema = object({
25730
- id: string(),
25731
- className: string(),
25732
- bbox: object({
25733
- x: number(),
25734
- y: number(),
25735
- w: number(),
25736
- h: number()
25737
- }),
25738
- frameWidth: number().int().nonnegative(),
25739
- frameHeight: number().int().nonnegative(),
25740
- /** When the source track was first seen. */
25741
- firstSeenAt: number().int(),
25742
- /** When the object was recognised as parked (promotion time). */
25743
- becameStationaryAt: number().int(),
25744
- /** Last frame a detection confirmed the object is still there. */
25745
- lastConfirmedAt: number().int(),
25746
- /** Enrichment label carried from the source track (identity / plate). */
25747
- label: string().optional(),
25748
- /** Native-resolution key-frame media key for the parked object's best image. */
25749
- keyFrameMediaKey: string().optional()
25750
- });
25751
- var CameraOccupancySnapshotSchema = object({
25752
- /** Frame timestamp of the inference result that produced this snapshot. */
25753
- ts: number().int(),
25754
- /** Frame width/height in pixels — let the UI normalize bbox coords. */
25755
- frameWidth: number().int().nonnegative(),
25756
- frameHeight: number().int().nonnegative(),
25757
- /** Per-zone breakdown — one entry per defined zone (user + onboard). */
25758
- zones: array(ZoneScopeBreakdownSchema).readonly(),
25759
- /** Frame-wide aggregate (everywhere, regardless of zone membership).
25760
- * INCLUDES currently-confirmed stationary objects (they are still present). */
25761
- frame: PerScopeBreakdownSchema,
25762
- /** Detections that landed outside every zone. Empty when no zones defined. */
25763
- unzoned: PerScopeBreakdownSchema,
25764
- /** Parked objects on this camera (additive — absent on legacy snapshots).
25765
- * Surfaced separately so the UI shows them in a dedicated section rather
25766
- * than as repeated tracks/events. */
25767
- stationaryObjects: array(StationaryObjectSchema).readonly().optional()
25768
- });
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
+ ]);
25769
25409
  /**
25770
- * Time-series resolution. The history methods return one bucket per
25771
- * step over the requested range. Smaller resolutions cost more
25772
- * memory + bandwidth; bound to discrete steps so caller cannot ask
25773
- * for arbitrary fractional buckets.
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.
25774
25417
  */
25775
- var HistoryResolutionEnum = _enum([
25776
- "minute",
25777
- "5min",
25778
- "hour"
25418
+ var KEYWORDS = new Set([
25419
+ "true",
25420
+ "false",
25421
+ "null"
25779
25422
  ]);
25780
- var HistoryRangeSchema = object({
25781
- /** Range start (epoch ms, inclusive). */
25782
- from: number().int(),
25783
- /** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
25784
- to: number().int(),
25785
- resolution: HistoryResolutionEnum
25786
- });
25787
- var HistoryPointSchema = object({
25788
- /** Bucket midpoint (epoch ms). */
25789
- ts: number().int(),
25790
- /** Object count averaged over the bucket (rounded to nearest integer). */
25791
- count: number().int().nonnegative()
25792
- });
25793
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
25794
- deviceId: number(),
25795
- zoneId: string(),
25796
- className: string().optional()
25797
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
25798
- deviceId: number(),
25799
- className: string().optional()
25800
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
25801
- deviceId: number(),
25802
- className: string().optional()
25803
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
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
+ }
25804
25551
  /**
25805
- * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
25806
- * cap so a single CRUD surface backs every consumer; each stage has
25807
- * its own dev-state mirror slice (`motion-zone-rules`,
25808
- * `detection-zone-rules`, …) so consumer addons subscribe independently.
25552
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
25809
25553
  *
25810
- * Extend the enum here when a new gating consumer comes online (audio
25811
- * gating, alert filtering, …) no other surface needs to change.
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`.
25812
25563
  */
25813
- var ZoneRuleStageEnum = _enum([
25814
- "motion",
25815
- "detection",
25816
- "package"
25817
- ]);
25818
- DeviceType.Camera, method(object({
25819
- deviceId: number(),
25820
- stage: ZoneRuleStageEnum
25821
- }), array(ZoneRuleSchema).readonly()), method(object({
25822
- deviceId: number(),
25823
- stage: ZoneRuleStageEnum,
25824
- rules: array(ZoneRuleSchema).readonly()
25825
- }), _void(), {
25826
- kind: "mutation",
25827
- auth: "admin"
25828
- }), object({
25829
- motion: array(ZoneRuleSchema).readonly(),
25830
- detection: array(ZoneRuleSchema).readonly(),
25831
- package: array(ZoneRuleSchema).readonly()
25832
- });
25833
- var HF_BASE_URL = `https://huggingface.co/camstack/camstack-models/resolve/main`;
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
+ }
25834
25754
  /**
25835
- * Accessory device helpers shared across drivers.
25836
- *
25837
- * Many vendor-specific drivers register accessory child devices on
25838
- * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
25839
- * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
25840
- * driver picks the right `DeviceType` + `DeviceRole` explicitly when
25841
- * spawning, builds a name derived from the parent, and produces a
25842
- * stableId tied to the parent so boot-restore can reconstruct the
25843
- * relationship.
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.
25844
25759
  *
25845
- * Centralised `(kind DeviceType)` mapping was dropped on purpose:
25846
- * drivers may reasonably disagree on the right type for an accessory
25847
- * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
25848
- * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
25849
- * one canonical mapping was over-prescriptive and added a layer of
25850
- * indirection without saving meaningful code at call sites — the
25851
- * driver knows its own hardware best.
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.
25852
25763
  */
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({});
25853
25797
  /**
25854
- * Subset of `DeviceRole` values that drivers register as child
25855
- * accessories of a parent device. Sourced verbatim from `DeviceRole`
25856
- * `AccessoryKind` is the alias drivers use when building accessory
25857
- * children, so the call site reads as
25858
- * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
25859
- * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
25860
- * any role works, including non-accessory ones like Doorbell).
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`.
25861
25803
  */
25862
- var AccessoryKind = {
25863
- Siren: DeviceRole.Siren,
25864
- Floodlight: DeviceRole.Floodlight,
25865
- Spotlight: DeviceRole.Spotlight,
25866
- PirSensor: DeviceRole.PirSensor,
25867
- Chime: DeviceRole.Chime,
25868
- Autotrack: DeviceRole.Autotrack,
25869
- Nightvision: DeviceRole.Nightvision,
25870
- PrivacyMask: DeviceRole.PrivacyMask
25871
- };
25872
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
25873
- 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;
25874
- new Set(Object.values(DeviceType));
25875
- DeviceFeature.BatteryOperated;
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()
25827
+ }),
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
+ })
25843
+ ]);
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
+ });
25876
25855
  Object.freeze({
25877
25856
  "accessories.setChildHidden": {
25878
25857
  capName: "accessories",
@@ -26696,6 +26675,12 @@ Object.freeze({
26696
26675
  addonId: null,
26697
26676
  access: "view"
26698
26677
  },
26678
+ "coreBlocks.restart": {
26679
+ capName: "core-blocks",
26680
+ capScope: "system",
26681
+ addonId: null,
26682
+ access: "create"
26683
+ },
26699
26684
  "coreBlocks.setEnabled": {
26700
26685
  capName: "core-blocks",
26701
26686
  capScope: "system",
@@ -27332,12 +27317,6 @@ Object.freeze({
27332
27317
  addonId: null,
27333
27318
  access: "create"
27334
27319
  },
27335
- "deviceManager.setDeviceLinks": {
27336
- capName: "device-manager",
27337
- capScope: "system",
27338
- addonId: null,
27339
- access: "create"
27340
- },
27341
27320
  "deviceManager.setDisabled": {
27342
27321
  capName: "device-manager",
27343
27322
  capScope: "system",
@@ -31318,249 +31297,62 @@ var DeviceIdsField = array(number()).min(1);
31318
31297
  var CadenceSecField = number().int().min(2).max(3600);
31319
31298
  var FramerateField = number().int().min(1).max(60);
31320
31299
  var TargetsField = array(NcRuleTargetSchema).min(1);
31321
- var PriorityField = number().int().min(1).max(5);
31322
- /**
31323
- * Client-supplied timelapse-rule fields. The server stamps id / createdBy /
31324
- * createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
31325
- * here (see the ownership note above).
31326
- */
31327
- var TimelapseRuleInputSchema = object({
31328
- name: NameField,
31329
- enabled: boolean().default(true),
31330
- /** Cameras sampled by this rule — one scratch dir + one artifact per device. */
31331
- deviceIds: DeviceIdsField,
31332
- /**
31333
- * Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
31334
- * means "always active"): a timelapse is defined by its window boundaries —
31335
- * open clears the scratch, close assembles and delivers.
31336
- */
31337
- schedule: NcScheduleSchema,
31338
- /** Force-snapshot cadence inside the window, seconds (predecessor parity). */
31339
- cadenceSec: CadenceSecField.default(15),
31340
- /** Output frames per second of the assembled mp4 (predecessor parity). */
31341
- framerate: FramerateField.default(10),
31342
- /** `notification-output` targets the finished video/thumbnail is sent to. */
31343
- targets: TargetsField,
31344
- template: TimelapseTemplateSchema.optional(),
31345
- /** Canonical notification priority ordinal (1..5); per-target overridable. */
31346
- priority: PriorityField.default(3)
31347
- });
31348
- object({
31349
- name: NameField.optional(),
31350
- enabled: boolean().optional(),
31351
- deviceIds: DeviceIdsField.optional(),
31352
- schedule: NcScheduleSchema.optional(),
31353
- cadenceSec: CadenceSecField.optional(),
31354
- framerate: FramerateField.optional(),
31355
- targets: TargetsField.optional(),
31356
- template: TimelapseTemplateSchema.nullable().optional(),
31357
- priority: PriorityField.optional()
31358
- });
31359
- TimelapseRuleInputSchema.extend({
31360
- id: string(),
31361
- /**
31362
- * Ownership/visibility key. Absent = admin/global rule (visible to all).
31363
- * Present = personal rule owned by this userId. Server-stamped from the
31364
- * resolved caller; never trusted from a client payload.
31365
- */
31366
- ownerUserId: string().optional(),
31367
- /**
31368
- * Epoch-ms of the last successful generation — the 1-hour re-generation
31369
- * guard's durable state (predecessor parity). Absent = never generated.
31370
- */
31371
- lastGeneratedAt: number().optional(),
31372
- /** userId of the caller who created the rule (server-stamped). */
31373
- createdBy: string(),
31374
- createdAt: number(),
31375
- updatedAt: number()
31376
- });
31377
- function hfModelUrl(repo, path) {
31378
- return `https://huggingface.co/${repo}/resolve/main/${path}`;
31379
- }
31380
- /**
31381
- * Names that, when used as URL query parameters, almost certainly carry
31382
- * a secret. Matching is case-insensitive and anchored to the full param
31383
- * name to avoid false positives like `user_id` (an entity reference, not
31384
- * a credential) or `topic=password-reset` (a value happens to contain
31385
- * the substring "password" but the param name is unrelated).
31386
- *
31387
- * Add new patterns here as we encounter cameras/APIs that leak through
31388
- * the existing list.
31389
- */
31390
- var CREDENTIAL_PARAM_PATTERNS = [
31391
- /^user$/i,
31392
- /^usr$/i,
31393
- /^username$/i,
31394
- /^password$/i,
31395
- /^pwd$/i,
31396
- /^pass$/i,
31397
- /^passwd$/i,
31398
- /^secret$/i,
31399
- /^token$/i,
31400
- /^auth$/i,
31401
- /^auth[_-]?token$/i,
31402
- /^bearer$/i,
31403
- /^api[_-]?key$/i,
31404
- /^access[_-]?token$/i,
31405
- /^refresh[_-]?token$/i
31406
- ];
31407
- function isCredentialParam(name) {
31408
- return CREDENTIAL_PARAM_PATTERNS.some((re) => re.test(name));
31409
- }
31410
- /**
31411
- * Redact secrets we may surface in logs or telemetry:
31412
- * 1. `userinfo` (`user:password@host`) — the classic RTSP shape.
31413
- * 2. Query-string credential params (`?user=...&password=...`) — the
31414
- * Reolink RTMP shape and several HTTP-pushed-stream APIs.
31415
- *
31416
- * Returns the original string verbatim when the input is not a parseable
31417
- * URL — a log helper must never throw. Param values are replaced with
31418
- * `***` so an operator reading logs still sees WHICH credential was
31419
- * present (useful for debugging "wrong password" issues) without
31420
- * exposing the value.
31421
- *
31422
- * The credential-name match is case-insensitive but anchored to the
31423
- * whole param name; values like `user_id=42` or `topic=password-reset`
31424
- * are NOT masked (those are entity references, not credentials).
31425
- */
31426
- function maskUrlCredentials(rawUrl) {
31427
- try {
31428
- const u = new URL(rawUrl);
31429
- let touched = false;
31430
- if (u.username || u.password) {
31431
- u.username = "";
31432
- u.password = "";
31433
- touched = true;
31434
- }
31435
- for (const key of Array.from(u.searchParams.keys())) if (isCredentialParam(key)) {
31436
- u.searchParams.set(key, "***");
31437
- touched = true;
31438
- }
31439
- return touched ? u.toString() : rawUrl;
31440
- } catch {
31441
- return rawUrl;
31442
- }
31443
- }
31444
- /**
31445
- * Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
31446
- * drain() returns up to maxCount items in FIFO order and removes them.
31447
- */
31448
- var RingBuffer = class {
31449
- capacity;
31450
- items;
31451
- head = 0;
31452
- tail = 0;
31453
- count = 0;
31454
- constructor(capacity) {
31455
- this.capacity = capacity;
31456
- this.items = Array.from({ length: capacity });
31457
- }
31458
- get size() {
31459
- return this.count;
31460
- }
31461
- push(item) {
31462
- this.items[this.tail] = item;
31463
- this.tail = (this.tail + 1) % this.capacity;
31464
- if (this.count < this.capacity) this.count++;
31465
- else this.head = (this.head + 1) % this.capacity;
31466
- }
31467
- drain(maxCount) {
31468
- const result = [];
31469
- const n = Math.min(maxCount, this.count);
31470
- for (let i = 0; i < n; i++) {
31471
- result.push(this.items[this.head]);
31472
- this.items[this.head] = void 0;
31473
- this.head = (this.head + 1) % this.capacity;
31474
- }
31475
- this.count -= n;
31476
- return result;
31477
- }
31478
- };
31479
- /**
31480
- * DEVICE-POOL backend → model format — the SSOT for the multi-device inference
31481
- * DESCRIPTOR path (Python per-device pools), distinct from {@link BACKEND_TO_FORMAT}
31482
- * above which is the legacy NODE runtime map (where `coreml → onnx` because
31483
- * onnxruntime-node's CoreML EP loads `.onnx`). Here a device pool runs the
31484
- * backend NATIVELY, so `coreml → coreml` and `edgetpu → tflite`. Consumed by
31485
- * `resolveDeviceEngine` (addon-pipeline) and the orchestrator's
31486
- * `parseDeviceKey`/`getNodeInferenceDevices` — the previously-duplicated
31487
- * `BACKEND_FORMAT` maps (R3/node-F2). `cpu`/`cuda` floor to onnx.
31488
- */
31489
- var DEVICE_BACKEND_TO_FORMAT = {
31490
- openvino: "openvino",
31491
- edgetpu: "tflite",
31492
- coreml: "coreml",
31493
- onnx: "onnx",
31494
- cpu: "onnx",
31495
- cuda: "onnx"
31496
- };
31497
- /**
31498
- * Evaluate `rules` against `items`. Returns the items partitioned
31499
- * into `passed` (kept by the gate) and `excluded` (dropped). When
31500
- * `rules` is empty or `zones` is empty everything passes — the gate
31501
- * is "off" without explicit configuration.
31502
- *
31503
- * `getCenter` returns the item's normalised (0–1) center point.
31504
- * `getClassName` is optional — when omitted, every rule's
31505
- * `classFilter` is treated as matching all classes (motion-stage
31506
- * semantics). Provide it for object detections so per-class rules
31507
- * narrow correctly.
31508
- */
31509
- function evaluateZoneRules(items, zones, rules, getCenter, getClassName) {
31510
- if (rules.length === 0 || zones.length === 0) return {
31511
- passed: items,
31512
- excluded: []
31513
- };
31514
- const activeRules = rules.filter((r) => r.enabled !== false);
31515
- const includeRules = activeRules.filter((r) => r.mode === "include");
31516
- const excludeRules = activeRules.filter((r) => r.mode === "exclude");
31517
- const whitelistMode = includeRules.length > 0;
31518
- const zonesById = new Map(zones.map((z) => [z.id, z]));
31519
- const passed = [];
31520
- const excluded = [];
31521
- for (const item of items) {
31522
- const center = getCenter(item);
31523
- const className = getClassName?.(item);
31524
- const inInclude = includeRules.some((r) => ruleApplies(r, center, className, zonesById));
31525
- const inExclude = excludeRules.some((r) => ruleApplies(r, center, className, zonesById));
31526
- if (whitelistMode) if (inInclude && !inExclude) passed.push(item);
31527
- else excluded.push(item);
31528
- else if (inExclude) excluded.push(item);
31529
- else passed.push(item);
31530
- }
31531
- return {
31532
- passed,
31533
- excluded
31534
- };
31535
- }
31536
- function ruleApplies(rule, center, className, zonesById) {
31537
- if (rule.classFilter && rule.classFilter.length > 0) {
31538
- if (className !== void 0 && !rule.classFilter.includes(className)) return false;
31539
- }
31540
- for (const zoneId of rule.zoneIds) {
31541
- const zone = zonesById.get(zoneId);
31542
- if (!zone) continue;
31543
- if (pointInPolygon(center, zone.polygon)) return true;
31544
- }
31545
- return false;
31546
- }
31300
+ var PriorityField = number().int().min(1).max(5);
31547
31301
  /**
31548
- * Standard ray-casting point-in-polygon. Polygon vertices are
31549
- * normalised (0–1); the test point is tooframe-resolution-
31550
- * agnostic. Boundary points count as inside (treats `<` as `<=` on
31551
- * 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).
31552
31305
  */
31553
- function pointInPolygon(point, polygon) {
31554
- if (polygon.length < 3) return false;
31555
- let inside = false;
31556
- const { x, y } = point;
31557
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
31558
- const a = polygon[i];
31559
- const b = polygon[j];
31560
- 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;
31561
- }
31562
- return inside;
31563
- }
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
+ });
31564
31356
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
31565
31357
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
31566
31358
  /**
@@ -31879,6 +31671,193 @@ function pickNativeLeaseOverride(view) {
31879
31671
  }
31880
31672
  return readNativeLeaseOverride(flat);
31881
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
+ }
31882
31861
  var AUTO = {
31883
31862
  value: "auto",
31884
31863
  label: "Auto"