@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.
- package/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-C9Q1t0nT.js → dist-CksKE1Sn.js} +1262 -1283
- package/dist/{dist-CfxC7XoI.mjs → dist-DoTw0NM3.mjs} +1262 -1283
- package/dist/{event-loop-stall-monitor-B_XUPTwp.mjs → event-loop-stall-monitor-CtktCf3u.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-Luq9AX4t.js → event-loop-stall-monitor-DzdwBr4X.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +1 -1
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- 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
- 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
- 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
- package/dist/stream-broker/{hostInit-D8gyxL0j.mjs → hostInit-DqD1qq8w.mjs} +2 -2
- package/dist/stream-broker/index.js +196 -130
- package/dist/stream-broker/index.mjs +196 -130
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-rqbyKlm9.mjs → worker-protocol-4_4L1QOe.mjs} +1 -1
- package/dist/{worker-protocol-BUJpm7l3.js → worker-protocol-CuEcTi66.js} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DwXIyaHw.js → MaskShapeCanvas-DI4BY7W2-EcWZ5m8e.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Cf8EtWEG.js → MotionZonesSettings-NcxxQN8r-CfiyCD3z.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BvsVXya9.js → PrivacyMaskSettings-APgPLF7p-ESPy4XXk.js} +1 -1
- package/embed-dist/assets/index-C3FKV1x-.js +112 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- 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
- 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
- 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/
|
|
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
|
-
*
|
|
10548
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
11824
|
-
*
|
|
11825
|
-
*
|
|
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
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
/**
|
|
11837
|
-
*
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
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
|
-
*
|
|
11846
|
-
*
|
|
11847
|
-
*
|
|
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
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
/**
|
|
11860
|
-
*
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
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
|
-
*
|
|
11869
|
-
*
|
|
11870
|
-
*
|
|
11871
|
-
*
|
|
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
|
-
*
|
|
11874
|
-
*
|
|
11875
|
-
*
|
|
11876
|
-
*
|
|
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
|
-
*
|
|
11880
|
-
*
|
|
11881
|
-
*
|
|
11882
|
-
*
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
25699
|
-
*
|
|
25700
|
-
*
|
|
25701
|
-
*
|
|
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
|
-
*
|
|
25704
|
-
*
|
|
25705
|
-
* `
|
|
25706
|
-
*
|
|
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
|
-
|
|
25709
|
-
|
|
25710
|
-
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
});
|
|
25714
|
-
|
|
25715
|
-
|
|
25716
|
-
|
|
25717
|
-
|
|
25718
|
-
|
|
25719
|
-
|
|
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
|
-
*
|
|
25723
|
-
*
|
|
25724
|
-
*
|
|
25725
|
-
*
|
|
25726
|
-
*
|
|
25727
|
-
*
|
|
25728
|
-
|
|
25729
|
-
|
|
25730
|
-
|
|
25731
|
-
|
|
25732
|
-
|
|
25733
|
-
|
|
25734
|
-
|
|
25735
|
-
|
|
25736
|
-
|
|
25737
|
-
|
|
25738
|
-
|
|
25739
|
-
|
|
25740
|
-
|
|
25741
|
-
|
|
25742
|
-
|
|
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
|
-
*
|
|
25771
|
-
*
|
|
25772
|
-
*
|
|
25773
|
-
*
|
|
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
|
|
25776
|
-
"
|
|
25777
|
-
"
|
|
25778
|
-
"
|
|
25418
|
+
var KEYWORDS = new Set([
|
|
25419
|
+
"true",
|
|
25420
|
+
"false",
|
|
25421
|
+
"null"
|
|
25779
25422
|
]);
|
|
25780
|
-
|
|
25781
|
-
|
|
25782
|
-
|
|
25783
|
-
|
|
25784
|
-
|
|
25785
|
-
|
|
25786
|
-
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25790
|
-
|
|
25791
|
-
|
|
25792
|
-
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
25802
|
-
|
|
25803
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
25811
|
-
*
|
|
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
|
-
|
|
25814
|
-
|
|
25815
|
-
"
|
|
25816
|
-
"
|
|
25817
|
-
|
|
25818
|
-
|
|
25819
|
-
|
|
25820
|
-
|
|
25821
|
-
|
|
25822
|
-
|
|
25823
|
-
|
|
25824
|
-
|
|
25825
|
-
|
|
25826
|
-
|
|
25827
|
-
|
|
25828
|
-
}
|
|
25829
|
-
|
|
25830
|
-
|
|
25831
|
-
|
|
25832
|
-
|
|
25833
|
-
|
|
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
|
-
*
|
|
25836
|
-
*
|
|
25837
|
-
*
|
|
25838
|
-
*
|
|
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
|
-
*
|
|
25846
|
-
*
|
|
25847
|
-
*
|
|
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
|
-
*
|
|
25855
|
-
*
|
|
25856
|
-
*
|
|
25857
|
-
*
|
|
25858
|
-
*
|
|
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
|
-
|
|
25863
|
-
|
|
25864
|
-
|
|
25865
|
-
|
|
25866
|
-
|
|
25867
|
-
|
|
25868
|
-
|
|
25869
|
-
|
|
25870
|
-
|
|
25871
|
-
|
|
25872
|
-
|
|
25873
|
-
|
|
25874
|
-
|
|
25875
|
-
|
|
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
|
-
*
|
|
31549
|
-
*
|
|
31550
|
-
*
|
|
31551
|
-
* the y-edge crossing).
|
|
31302
|
+
* Client-supplied timelapse-rule fields. The server stamps id / createdBy /
|
|
31303
|
+
* createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
|
|
31304
|
+
* here (see the ownership note above).
|
|
31552
31305
|
*/
|
|
31553
|
-
|
|
31554
|
-
|
|
31555
|
-
|
|
31556
|
-
|
|
31557
|
-
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
31562
|
-
|
|
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"
|