@camstack/addon-export-hap 1.2.15 → 1.2.17
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/export-hap.addon.js +1471 -886
- package/dist/export-hap.addon.mjs +1471 -887
- package/package.json +1 -1
|
@@ -7054,7 +7054,7 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
|
|
|
7054
7054
|
input: unknown()
|
|
7055
7055
|
}), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
|
|
7056
7056
|
//#endregion
|
|
7057
|
-
//#region ../types/dist/
|
|
7057
|
+
//#region ../types/dist/canonical-hash-rO1sRmEK.mjs
|
|
7058
7058
|
var AUDIO_ENCODER_BY_CODEC = {
|
|
7059
7059
|
opus: "libopus",
|
|
7060
7060
|
aac: "aac",
|
|
@@ -7348,37 +7348,6 @@ function buildFfmpegArgs(inv) {
|
|
|
7348
7348
|
...inv.audioSidecar ? buildAudioSidecarArgs(inv.audioSidecar) : []
|
|
7349
7349
|
];
|
|
7350
7350
|
}
|
|
7351
|
-
/**
|
|
7352
|
-
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
7353
|
-
* canonical form sorts object keys alphabetically at every depth so two
|
|
7354
|
-
* structurally-equal inputs with different key insertion orders produce
|
|
7355
|
-
* the same hash. Returns a 64-char lowercase hex digest.
|
|
7356
|
-
*
|
|
7357
|
-
* Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
|
|
7358
|
-
* accessory-rebuild work when the upstream shape is byte-identical to
|
|
7359
|
-
* the last applied state — preventing user-visible "re-discovery"
|
|
7360
|
-
* notifications on every addon-runner respawn. Each respawn re-fires
|
|
7361
|
-
* `DeviceBindingsChanged` for every cap registration, which without
|
|
7362
|
-
* this guard would propagate redundant pushes.
|
|
7363
|
-
*
|
|
7364
|
-
* Note: this is a SYMPTOMATIC fix layered on top of the binding-change
|
|
7365
|
-
* subscription. The proper fix is a single "device ready" lifecycle
|
|
7366
|
-
* barrier so exports react only when the full cap set has landed —
|
|
7367
|
-
* tracked separately for post-HA-integration work.
|
|
7368
|
-
*/
|
|
7369
|
-
function canonicalHash(value) {
|
|
7370
|
-
const canonical = JSON.stringify(value, replaceWithSortedKeys);
|
|
7371
|
-
return createHash("sha256").update(canonical ?? "").digest("hex");
|
|
7372
|
-
}
|
|
7373
|
-
function replaceWithSortedKeys(_key, value) {
|
|
7374
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
7375
|
-
const obj = value;
|
|
7376
|
-
const out = {};
|
|
7377
|
-
for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
|
|
7378
|
-
return out;
|
|
7379
|
-
}
|
|
7380
|
-
return value;
|
|
7381
|
-
}
|
|
7382
7351
|
var DEFAULT_MAX_UNIT_BYTES = 16 * 1024 * 1024;
|
|
7383
7352
|
/** Header size for a normal box, and for one carrying a 64-bit `largesize`. */
|
|
7384
7353
|
var BOX_HEADER_BYTES = 8;
|
|
@@ -7551,6 +7520,37 @@ var Fmp4BoxSplitter = class {
|
|
|
7551
7520
|
return [];
|
|
7552
7521
|
}
|
|
7553
7522
|
};
|
|
7523
|
+
/**
|
|
7524
|
+
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
7525
|
+
* canonical form sorts object keys alphabetically at every depth so two
|
|
7526
|
+
* structurally-equal inputs with different key insertion orders produce
|
|
7527
|
+
* the same hash. Returns a 64-char lowercase hex digest.
|
|
7528
|
+
*
|
|
7529
|
+
* Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
|
|
7530
|
+
* accessory-rebuild work when the upstream shape is byte-identical to
|
|
7531
|
+
* the last applied state — preventing user-visible "re-discovery"
|
|
7532
|
+
* notifications on every addon-runner respawn. Each respawn re-fires
|
|
7533
|
+
* `DeviceBindingsChanged` for every cap registration, which without
|
|
7534
|
+
* this guard would propagate redundant pushes.
|
|
7535
|
+
*
|
|
7536
|
+
* Note: this is a SYMPTOMATIC fix layered on top of the binding-change
|
|
7537
|
+
* subscription. The proper fix is a single "device ready" lifecycle
|
|
7538
|
+
* barrier so exports react only when the full cap set has landed —
|
|
7539
|
+
* tracked separately for post-HA-integration work.
|
|
7540
|
+
*/
|
|
7541
|
+
function canonicalHash(value) {
|
|
7542
|
+
const canonical = JSON.stringify(value, replaceWithSortedKeys);
|
|
7543
|
+
return createHash("sha256").update(canonical ?? "").digest("hex");
|
|
7544
|
+
}
|
|
7545
|
+
function replaceWithSortedKeys(_key, value) {
|
|
7546
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
7547
|
+
const obj = value;
|
|
7548
|
+
const out = {};
|
|
7549
|
+
for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
|
|
7550
|
+
return out;
|
|
7551
|
+
}
|
|
7552
|
+
return value;
|
|
7553
|
+
}
|
|
7554
7554
|
//#endregion
|
|
7555
7555
|
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
7556
7556
|
/**
|
|
@@ -9630,6 +9630,69 @@ var StreamFormatSchema = _enum([
|
|
|
9630
9630
|
"mjpeg",
|
|
9631
9631
|
"rtsp"
|
|
9632
9632
|
]);
|
|
9633
|
+
/** A container `produceEventMedia` can emit. */
|
|
9634
|
+
var EventMediaKindSchema = _enum(["mp4", "gif"]);
|
|
9635
|
+
/**
|
|
9636
|
+
* One produced artifact, referenced by HANDLE.
|
|
9637
|
+
*
|
|
9638
|
+
* Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
|
|
9639
|
+
* method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
|
|
9640
|
+
* [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
|
|
9641
|
+
* on demand, compressed, by handle). `bytes` is here so a caller can decide
|
|
9642
|
+
* whether it wants the fetch at all.
|
|
9643
|
+
*/
|
|
9644
|
+
var EventMediaArtifactSchema = object({
|
|
9645
|
+
kind: EventMediaKindSchema,
|
|
9646
|
+
/** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
|
|
9647
|
+
handle: string(),
|
|
9648
|
+
/**
|
|
9649
|
+
* The node holding the bytes — the ROUTING key for `fetchEventMedia`.
|
|
9650
|
+
*
|
|
9651
|
+
* `stream-broker` is a singleton cap and an unpinned call never leaves the
|
|
9652
|
+
* hub, so a handle produced on an agent's broker would be redeemed against
|
|
9653
|
+
* the hub's store and come back `null`. Same contract, same field name and
|
|
9654
|
+
* the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
|
|
9655
|
+
* lives and the consumer pins to it.
|
|
9656
|
+
*/
|
|
9657
|
+
nodeId: string(),
|
|
9658
|
+
mime: string(),
|
|
9659
|
+
bytes: number().int(),
|
|
9660
|
+
width: number().int(),
|
|
9661
|
+
height: number().int()
|
|
9662
|
+
});
|
|
9663
|
+
/**
|
|
9664
|
+
* What a production actually covered — the answer to the only question an
|
|
9665
|
+
* operator asks about a notification clip.
|
|
9666
|
+
*
|
|
9667
|
+
* `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
|
|
9668
|
+
* so a caller can state "this clip starts 4.1 s before the event" instead of
|
|
9669
|
+
* inferring it from a duration. A production whose `fromTs` is later than the
|
|
9670
|
+
* event is a production with no pre-roll, and that is exactly the defect this
|
|
9671
|
+
* method exists to make visible rather than plausible.
|
|
9672
|
+
*/
|
|
9673
|
+
var EventMediaCoverageSchema = object({
|
|
9674
|
+
fromTs: number(),
|
|
9675
|
+
toTs: number(),
|
|
9676
|
+
/** Encoded packets in the muxed window. */
|
|
9677
|
+
packets: number().int()
|
|
9678
|
+
});
|
|
9679
|
+
/**
|
|
9680
|
+
* The result of ONE cut, in every container the caller asked for.
|
|
9681
|
+
*
|
|
9682
|
+
* Every artifact in `media` came out of the SAME window of the SAME rendition —
|
|
9683
|
+
* that is the whole reason this is one method rather than one call per format.
|
|
9684
|
+
* A consumer attaching a gif and a video can no longer show two different
|
|
9685
|
+
* moments, because it never chose two sources.
|
|
9686
|
+
*/
|
|
9687
|
+
var EventMediaProductionSchema = object({
|
|
9688
|
+
media: array(EventMediaArtifactSchema).readonly(),
|
|
9689
|
+
coverage: EventMediaCoverageSchema,
|
|
9690
|
+
/** The rendition actually cut from — what the default or the fallback chose. */
|
|
9691
|
+
profile: CamProfileSchema,
|
|
9692
|
+
/** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
|
|
9693
|
+
* source, a downscale, or a playback rate other than 1). */
|
|
9694
|
+
video: _enum(["copy", "encode"])
|
|
9695
|
+
});
|
|
9633
9696
|
var RtspRestreamEntrySchema = object({
|
|
9634
9697
|
brokerId: string(),
|
|
9635
9698
|
url: string(),
|
|
@@ -10029,6 +10092,56 @@ method(object({
|
|
|
10029
10092
|
}), {
|
|
10030
10093
|
kind: "mutation",
|
|
10031
10094
|
auth: "admin"
|
|
10095
|
+
}), method(object({
|
|
10096
|
+
deviceId: number(),
|
|
10097
|
+
/** Absent = the largest H.264 rendition at or below 1080p, which is
|
|
10098
|
+
* also the one that can be copied. Falls back to whatever the ring
|
|
10099
|
+
* actually retained, and the answer says which. */
|
|
10100
|
+
profile: CamProfileSchema.optional(),
|
|
10101
|
+
aroundMs: number(),
|
|
10102
|
+
preSeconds: number().min(0).max(20).default(4),
|
|
10103
|
+
postSeconds: number().min(0).max(20).default(6),
|
|
10104
|
+
kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
|
|
10105
|
+
/** GIF geometry. The video keeps the source's own. */
|
|
10106
|
+
gifMaxWidth: number().int().min(120).max(1280).default(640),
|
|
10107
|
+
/**
|
|
10108
|
+
* The gif's own PLAYBACK rate in frames per second — what the finished
|
|
10109
|
+
* gif runs at, not how many source frames feed it. The decimation that
|
|
10110
|
+
* feeds it samples `gifFps / gifSpeed` source frames per second, so at
|
|
10111
|
+
* the defaults a 12 fps gif is built out of 3 source frames a second.
|
|
10112
|
+
*/
|
|
10113
|
+
gifFps: number().int().min(1).max(15).default(12),
|
|
10114
|
+
/**
|
|
10115
|
+
* How fast the GIF plays against real time, independent of `speed`.
|
|
10116
|
+
*
|
|
10117
|
+
* 4× by default, by operator request: a notification gif is glanced at
|
|
10118
|
+
* on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
|
|
10119
|
+
* a separate knob from `speed` even though both now default to 4 —
|
|
10120
|
+
* a caller wanting a real-time video and a fast gif must not have to
|
|
10121
|
+
* choose.
|
|
10122
|
+
*/
|
|
10123
|
+
gifSpeed: number().min(1).max(8).default(4),
|
|
10124
|
+
/**
|
|
10125
|
+
* Playback rate of the VIDEO. Also 4× by default, by operator decision.
|
|
10126
|
+
*
|
|
10127
|
+
* `1` is real time and is the ONLY value that allows the copy branch —
|
|
10128
|
+
* anything else forces `libx264` over the window. That was priced
|
|
10129
|
+
* before it was chosen: a per-event burst measured at 0.23 s and 254 KB
|
|
10130
|
+
* on a real 615 720p cut, against 922 KB for the copy it replaces. A
|
|
10131
|
+
* re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
|
|
10132
|
+
* once the decode is forced the width stops being free.
|
|
10133
|
+
*/
|
|
10134
|
+
speed: number().min(1).max(8).default(4)
|
|
10135
|
+
}), EventMediaProductionSchema, {
|
|
10136
|
+
kind: "mutation",
|
|
10137
|
+
auth: "admin"
|
|
10138
|
+
}), method(object({ handle: string() }), object({
|
|
10139
|
+
base64: string(),
|
|
10140
|
+
mime: string(),
|
|
10141
|
+
bytes: number().int()
|
|
10142
|
+
}).nullable(), {
|
|
10143
|
+
kind: "mutation",
|
|
10144
|
+
auth: "admin"
|
|
10032
10145
|
}), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
|
|
10033
10146
|
probed: boolean(),
|
|
10034
10147
|
summary: string()
|
|
@@ -10221,25 +10334,6 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
|
|
|
10221
10334
|
}),
|
|
10222
10335
|
lastChangedAt: number()
|
|
10223
10336
|
});
|
|
10224
|
-
/**
|
|
10225
|
-
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
10226
|
-
* its own process.
|
|
10227
|
-
*
|
|
10228
|
-
* Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
|
|
10229
|
-
*
|
|
10230
|
-
* The first use is **owning devices without being a device provider**: a block
|
|
10231
|
-
* declares devices under a system or custom integration and drives their state,
|
|
10232
|
-
* with the same `ctx` an addon gets. Automations come later; nothing here
|
|
10233
|
-
* models a trigger.
|
|
10234
|
-
*
|
|
10235
|
-
* **Stated plainly, because it does not change by being true:** a block has an
|
|
10236
|
-
* addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
|
|
10237
|
-
* with no review step. What makes that survivable is not a sandbox, it is
|
|
10238
|
-
* PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
|
|
10239
|
-
* so a block that throws or never returns is marked `failed` and visible
|
|
10240
|
-
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
10241
|
-
* must stay so.
|
|
10242
|
-
*/
|
|
10243
10337
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
10244
10338
|
* agent is the reason placement is not fixed to the hub. */
|
|
10245
10339
|
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
@@ -10311,6 +10405,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
10311
10405
|
}), object({ block: CoreBlockSchema }), {
|
|
10312
10406
|
kind: "mutation",
|
|
10313
10407
|
auth: "admin"
|
|
10408
|
+
}), method(object({ blockId: string() }), object({ block: CoreBlockSchema }), {
|
|
10409
|
+
kind: "mutation",
|
|
10410
|
+
auth: "admin"
|
|
10314
10411
|
}), method(object({ code: string() }), CoreBlockCompileResultSchema, {
|
|
10315
10412
|
kind: "mutation",
|
|
10316
10413
|
auth: "admin"
|
|
@@ -11070,601 +11167,6 @@ var deviceExportCapability = {
|
|
|
11070
11167
|
unexposeDevice: method(UnexposeInputSchema, _void(), { kind: "mutation" })
|
|
11071
11168
|
}
|
|
11072
11169
|
};
|
|
11073
|
-
/**
|
|
11074
|
-
* Resource-bound constants for the safe expression engine.
|
|
11075
|
-
*
|
|
11076
|
-
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
11077
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
11078
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
11079
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
11080
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
11081
|
-
*/
|
|
11082
|
-
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
11083
|
-
* rejected without allocation. */
|
|
11084
|
-
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
11085
|
-
/** A legal binding / identifier name. */
|
|
11086
|
-
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11087
|
-
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
11088
|
-
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
11089
|
-
var RESERVED_BINDING_NAMES = new Set([
|
|
11090
|
-
"now",
|
|
11091
|
-
"true",
|
|
11092
|
-
"false",
|
|
11093
|
-
"null"
|
|
11094
|
-
]);
|
|
11095
|
-
/**
|
|
11096
|
-
* Error types for the safe expression engine. Two distinct classes so callers
|
|
11097
|
-
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
11098
|
-
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
11099
|
-
*/
|
|
11100
|
-
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
11101
|
-
* the failure is anchored to a character (author-facing inline feedback). */
|
|
11102
|
-
var ExpressionParseError = class extends Error {
|
|
11103
|
-
position;
|
|
11104
|
-
constructor(message, position) {
|
|
11105
|
-
super(message);
|
|
11106
|
-
this.name = "ExpressionParseError";
|
|
11107
|
-
this.position = position;
|
|
11108
|
-
}
|
|
11109
|
-
};
|
|
11110
|
-
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
11111
|
-
* result, unknown builtin, step-budget exceeded). */
|
|
11112
|
-
var ExpressionEvalError = class extends Error {
|
|
11113
|
-
constructor(message) {
|
|
11114
|
-
super(message);
|
|
11115
|
-
this.name = "ExpressionEvalError";
|
|
11116
|
-
}
|
|
11117
|
-
};
|
|
11118
|
-
/**
|
|
11119
|
-
* Frozen, null-prototype builtin function table for the expression engine
|
|
11120
|
-
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
11121
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
11122
|
-
* own-property check against it.
|
|
11123
|
-
*
|
|
11124
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
11125
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
11126
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
11127
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
11128
|
-
* callable — they are simply "unknown function" at parse time.
|
|
11129
|
-
*
|
|
11130
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
11131
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
11132
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
11133
|
-
* closed rather than emitting a garbage value.
|
|
11134
|
-
*/
|
|
11135
|
-
function asFiniteNumber(value, name, index) {
|
|
11136
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
11137
|
-
return value;
|
|
11138
|
-
}
|
|
11139
|
-
function asString$1(value, name, index) {
|
|
11140
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
11141
|
-
return value;
|
|
11142
|
-
}
|
|
11143
|
-
function finiteResult(value, name) {
|
|
11144
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
11145
|
-
return value;
|
|
11146
|
-
}
|
|
11147
|
-
function allFiniteNumbers(args, name) {
|
|
11148
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
11149
|
-
}
|
|
11150
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
11151
|
-
var table = {
|
|
11152
|
-
min: {
|
|
11153
|
-
minArgs: 1,
|
|
11154
|
-
maxArgs: INF,
|
|
11155
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
11156
|
-
},
|
|
11157
|
-
max: {
|
|
11158
|
-
minArgs: 1,
|
|
11159
|
-
maxArgs: INF,
|
|
11160
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
11161
|
-
},
|
|
11162
|
-
abs: {
|
|
11163
|
-
minArgs: 1,
|
|
11164
|
-
maxArgs: 1,
|
|
11165
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
11166
|
-
},
|
|
11167
|
-
floor: {
|
|
11168
|
-
minArgs: 1,
|
|
11169
|
-
maxArgs: 1,
|
|
11170
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
11171
|
-
},
|
|
11172
|
-
ceil: {
|
|
11173
|
-
minArgs: 1,
|
|
11174
|
-
maxArgs: 1,
|
|
11175
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
11176
|
-
},
|
|
11177
|
-
sqrt: {
|
|
11178
|
-
minArgs: 1,
|
|
11179
|
-
maxArgs: 1,
|
|
11180
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
11181
|
-
},
|
|
11182
|
-
round: {
|
|
11183
|
-
minArgs: 1,
|
|
11184
|
-
maxArgs: 2,
|
|
11185
|
-
apply: (args) => {
|
|
11186
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
11187
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
11188
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
11189
|
-
const factor = 10 ** digits;
|
|
11190
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
11191
|
-
}
|
|
11192
|
-
},
|
|
11193
|
-
pow: {
|
|
11194
|
-
minArgs: 2,
|
|
11195
|
-
maxArgs: 2,
|
|
11196
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
11197
|
-
},
|
|
11198
|
-
clamp: {
|
|
11199
|
-
minArgs: 3,
|
|
11200
|
-
maxArgs: 3,
|
|
11201
|
-
apply: (args) => {
|
|
11202
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
11203
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
11204
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
11205
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
11206
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
11207
|
-
}
|
|
11208
|
-
},
|
|
11209
|
-
avg: {
|
|
11210
|
-
minArgs: 1,
|
|
11211
|
-
maxArgs: INF,
|
|
11212
|
-
apply: (args) => {
|
|
11213
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
11214
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
11215
|
-
}
|
|
11216
|
-
},
|
|
11217
|
-
sum: {
|
|
11218
|
-
minArgs: 1,
|
|
11219
|
-
maxArgs: INF,
|
|
11220
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
11221
|
-
},
|
|
11222
|
-
coalesce: {
|
|
11223
|
-
minArgs: 1,
|
|
11224
|
-
maxArgs: INF,
|
|
11225
|
-
apply: (args) => {
|
|
11226
|
-
for (const a of args) if (a !== null) return a;
|
|
11227
|
-
return null;
|
|
11228
|
-
}
|
|
11229
|
-
},
|
|
11230
|
-
age: {
|
|
11231
|
-
minArgs: 2,
|
|
11232
|
-
maxArgs: 2,
|
|
11233
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
11234
|
-
},
|
|
11235
|
-
convert: {
|
|
11236
|
-
minArgs: 3,
|
|
11237
|
-
maxArgs: 3,
|
|
11238
|
-
apply: (args, hooks) => {
|
|
11239
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
11240
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
11241
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
11242
|
-
if (hooks.convert) {
|
|
11243
|
-
const out = hooks.convert(x, from, to);
|
|
11244
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
11245
|
-
return finiteResult(out, "convert");
|
|
11246
|
-
}
|
|
11247
|
-
if (from === to) return x;
|
|
11248
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
11249
|
-
}
|
|
11250
|
-
}
|
|
11251
|
-
};
|
|
11252
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
11253
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
11254
|
-
* callees at parse time (immediate author feedback). */
|
|
11255
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
11256
|
-
/**
|
|
11257
|
-
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
11258
|
-
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
11259
|
-
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
11260
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
11261
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
11262
|
-
* is a parse error with a source position, so member access / assignment /
|
|
11263
|
-
* template literals are lexically impossible.
|
|
11264
|
-
*/
|
|
11265
|
-
var KEYWORDS = new Set([
|
|
11266
|
-
"true",
|
|
11267
|
-
"false",
|
|
11268
|
-
"null"
|
|
11269
|
-
]);
|
|
11270
|
-
function isDigit(ch) {
|
|
11271
|
-
return ch >= "0" && ch <= "9";
|
|
11272
|
-
}
|
|
11273
|
-
function isIdentStart(ch) {
|
|
11274
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
11275
|
-
}
|
|
11276
|
-
function isIdentPart(ch) {
|
|
11277
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
11278
|
-
}
|
|
11279
|
-
function isWhitespace(ch) {
|
|
11280
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
11281
|
-
}
|
|
11282
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
11283
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
11284
|
-
* string. */
|
|
11285
|
-
function tokenize(source) {
|
|
11286
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
11287
|
-
const tokens = [];
|
|
11288
|
-
let i = 0;
|
|
11289
|
-
const n = source.length;
|
|
11290
|
-
while (i < n) {
|
|
11291
|
-
const ch = source[i];
|
|
11292
|
-
if (isWhitespace(ch)) {
|
|
11293
|
-
i += 1;
|
|
11294
|
-
continue;
|
|
11295
|
-
}
|
|
11296
|
-
if (isDigit(ch)) {
|
|
11297
|
-
const start = i;
|
|
11298
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
11299
|
-
if (i < n && source[i] === ".") {
|
|
11300
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
11301
|
-
i += 1;
|
|
11302
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
11303
|
-
}
|
|
11304
|
-
const text = source.slice(start, i);
|
|
11305
|
-
const value = Number(text);
|
|
11306
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
11307
|
-
tokens.push({
|
|
11308
|
-
type: "number",
|
|
11309
|
-
value,
|
|
11310
|
-
pos: start
|
|
11311
|
-
});
|
|
11312
|
-
continue;
|
|
11313
|
-
}
|
|
11314
|
-
if (ch === "'" || ch === "\"") {
|
|
11315
|
-
const quote = ch;
|
|
11316
|
-
const start = i;
|
|
11317
|
-
i += 1;
|
|
11318
|
-
let out = "";
|
|
11319
|
-
let closed = false;
|
|
11320
|
-
while (i < n) {
|
|
11321
|
-
const c = source[i];
|
|
11322
|
-
if (c === "\\") {
|
|
11323
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
11324
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
11325
|
-
out += next;
|
|
11326
|
-
i += 2;
|
|
11327
|
-
continue;
|
|
11328
|
-
}
|
|
11329
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
11330
|
-
}
|
|
11331
|
-
if (c === quote) {
|
|
11332
|
-
closed = true;
|
|
11333
|
-
i += 1;
|
|
11334
|
-
break;
|
|
11335
|
-
}
|
|
11336
|
-
out += c;
|
|
11337
|
-
i += 1;
|
|
11338
|
-
}
|
|
11339
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
11340
|
-
tokens.push({
|
|
11341
|
-
type: "string",
|
|
11342
|
-
value: out,
|
|
11343
|
-
pos: start
|
|
11344
|
-
});
|
|
11345
|
-
continue;
|
|
11346
|
-
}
|
|
11347
|
-
if (isIdentStart(ch)) {
|
|
11348
|
-
const start = i;
|
|
11349
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
11350
|
-
const text = source.slice(start, i);
|
|
11351
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
11352
|
-
type: "keyword",
|
|
11353
|
-
keyword: keywordOf(text),
|
|
11354
|
-
pos: start
|
|
11355
|
-
});
|
|
11356
|
-
else tokens.push({
|
|
11357
|
-
type: "identifier",
|
|
11358
|
-
name: text,
|
|
11359
|
-
pos: start
|
|
11360
|
-
});
|
|
11361
|
-
continue;
|
|
11362
|
-
}
|
|
11363
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
11364
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
11365
|
-
tokens.push({
|
|
11366
|
-
type: "punct",
|
|
11367
|
-
punct: two,
|
|
11368
|
-
pos: i
|
|
11369
|
-
});
|
|
11370
|
-
i += 2;
|
|
11371
|
-
continue;
|
|
11372
|
-
}
|
|
11373
|
-
if (isSinglePunct(ch)) {
|
|
11374
|
-
tokens.push({
|
|
11375
|
-
type: "punct",
|
|
11376
|
-
punct: ch,
|
|
11377
|
-
pos: i
|
|
11378
|
-
});
|
|
11379
|
-
i += 1;
|
|
11380
|
-
continue;
|
|
11381
|
-
}
|
|
11382
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
11383
|
-
}
|
|
11384
|
-
tokens.push({
|
|
11385
|
-
type: "eof",
|
|
11386
|
-
pos: n
|
|
11387
|
-
});
|
|
11388
|
-
return tokens;
|
|
11389
|
-
}
|
|
11390
|
-
function keywordOf(text) {
|
|
11391
|
-
if (text === "true") return "true";
|
|
11392
|
-
if (text === "false") return "false";
|
|
11393
|
-
return "null";
|
|
11394
|
-
}
|
|
11395
|
-
function isSinglePunct(ch) {
|
|
11396
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
11397
|
-
}
|
|
11398
|
-
/**
|
|
11399
|
-
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
11400
|
-
*
|
|
11401
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
11402
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
11403
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
11404
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
11405
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
11406
|
-
* that references a since-removed builtin degrades at read.
|
|
11407
|
-
*
|
|
11408
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
11409
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
11410
|
-
*/
|
|
11411
|
-
/** Binary/logical operator precedence (higher binds tighter). */
|
|
11412
|
-
var BINARY_PRECEDENCE = {
|
|
11413
|
-
"||": 1,
|
|
11414
|
-
"&&": 2,
|
|
11415
|
-
"==": 3,
|
|
11416
|
-
"!=": 3,
|
|
11417
|
-
"<": 4,
|
|
11418
|
-
"<=": 4,
|
|
11419
|
-
">": 4,
|
|
11420
|
-
">=": 4,
|
|
11421
|
-
"+": 5,
|
|
11422
|
-
"-": 5,
|
|
11423
|
-
"*": 6,
|
|
11424
|
-
"/": 6,
|
|
11425
|
-
"%": 6
|
|
11426
|
-
};
|
|
11427
|
-
function isLogicalOp(op) {
|
|
11428
|
-
return op === "&&" || op === "||";
|
|
11429
|
-
}
|
|
11430
|
-
function isBinaryOp(op) {
|
|
11431
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
11432
|
-
}
|
|
11433
|
-
var Parser = class {
|
|
11434
|
-
tokens;
|
|
11435
|
-
pos = 0;
|
|
11436
|
-
nodeCount = 0;
|
|
11437
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
11438
|
-
callees = /* @__PURE__ */ new Set();
|
|
11439
|
-
constructor(tokens) {
|
|
11440
|
-
this.tokens = tokens;
|
|
11441
|
-
}
|
|
11442
|
-
parse() {
|
|
11443
|
-
const ast = this.parseTernary();
|
|
11444
|
-
const tok = this.peek();
|
|
11445
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
11446
|
-
return {
|
|
11447
|
-
ast,
|
|
11448
|
-
identifiers: this.identifiers,
|
|
11449
|
-
callees: this.callees,
|
|
11450
|
-
nodeCount: this.nodeCount
|
|
11451
|
-
};
|
|
11452
|
-
}
|
|
11453
|
-
peek() {
|
|
11454
|
-
return this.tokens[this.pos];
|
|
11455
|
-
}
|
|
11456
|
-
next() {
|
|
11457
|
-
return this.tokens[this.pos++];
|
|
11458
|
-
}
|
|
11459
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
11460
|
-
expectPunct(punct) {
|
|
11461
|
-
const tok = this.peek();
|
|
11462
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
11463
|
-
this.pos += 1;
|
|
11464
|
-
}
|
|
11465
|
-
matchPunct(punct) {
|
|
11466
|
-
const tok = this.peek();
|
|
11467
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
11468
|
-
this.pos += 1;
|
|
11469
|
-
return true;
|
|
11470
|
-
}
|
|
11471
|
-
return false;
|
|
11472
|
-
}
|
|
11473
|
-
countNode() {
|
|
11474
|
-
this.nodeCount += 1;
|
|
11475
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
11476
|
-
}
|
|
11477
|
-
parseTernary() {
|
|
11478
|
-
const test = this.parseBinary(1);
|
|
11479
|
-
if (this.matchPunct("?")) {
|
|
11480
|
-
const consequent = this.parseTernary();
|
|
11481
|
-
this.expectPunct(":");
|
|
11482
|
-
const alternate = this.parseTernary();
|
|
11483
|
-
this.countNode();
|
|
11484
|
-
return {
|
|
11485
|
-
kind: "conditional",
|
|
11486
|
-
test,
|
|
11487
|
-
consequent,
|
|
11488
|
-
alternate
|
|
11489
|
-
};
|
|
11490
|
-
}
|
|
11491
|
-
return test;
|
|
11492
|
-
}
|
|
11493
|
-
parseBinary(minPrec) {
|
|
11494
|
-
let left = this.parseUnary();
|
|
11495
|
-
for (;;) {
|
|
11496
|
-
const tok = this.peek();
|
|
11497
|
-
if (tok.type !== "punct") break;
|
|
11498
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
11499
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
11500
|
-
const op = tok.punct;
|
|
11501
|
-
this.pos += 1;
|
|
11502
|
-
const right = this.parseBinary(prec + 1);
|
|
11503
|
-
this.countNode();
|
|
11504
|
-
if (isLogicalOp(op)) left = {
|
|
11505
|
-
kind: "logical",
|
|
11506
|
-
op,
|
|
11507
|
-
left,
|
|
11508
|
-
right
|
|
11509
|
-
};
|
|
11510
|
-
else if (isBinaryOp(op)) left = {
|
|
11511
|
-
kind: "binary",
|
|
11512
|
-
op,
|
|
11513
|
-
left,
|
|
11514
|
-
right
|
|
11515
|
-
};
|
|
11516
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
11517
|
-
}
|
|
11518
|
-
return left;
|
|
11519
|
-
}
|
|
11520
|
-
parseUnary() {
|
|
11521
|
-
const tok = this.peek();
|
|
11522
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
11523
|
-
const op = tok.punct;
|
|
11524
|
-
this.pos += 1;
|
|
11525
|
-
const operand = this.parseUnary();
|
|
11526
|
-
this.countNode();
|
|
11527
|
-
return {
|
|
11528
|
-
kind: "unary",
|
|
11529
|
-
op,
|
|
11530
|
-
operand
|
|
11531
|
-
};
|
|
11532
|
-
}
|
|
11533
|
-
return this.parsePrimary();
|
|
11534
|
-
}
|
|
11535
|
-
parsePrimary() {
|
|
11536
|
-
const tok = this.next();
|
|
11537
|
-
switch (tok.type) {
|
|
11538
|
-
case "number":
|
|
11539
|
-
this.countNode();
|
|
11540
|
-
return {
|
|
11541
|
-
kind: "literal",
|
|
11542
|
-
value: tok.value
|
|
11543
|
-
};
|
|
11544
|
-
case "string":
|
|
11545
|
-
this.countNode();
|
|
11546
|
-
return {
|
|
11547
|
-
kind: "literal",
|
|
11548
|
-
value: tok.value
|
|
11549
|
-
};
|
|
11550
|
-
case "keyword":
|
|
11551
|
-
this.countNode();
|
|
11552
|
-
return {
|
|
11553
|
-
kind: "literal",
|
|
11554
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
11555
|
-
};
|
|
11556
|
-
case "identifier": {
|
|
11557
|
-
const nextTok = this.peek();
|
|
11558
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
11559
|
-
this.identifiers.add(tok.name);
|
|
11560
|
-
this.countNode();
|
|
11561
|
-
return {
|
|
11562
|
-
kind: "identifier",
|
|
11563
|
-
name: tok.name
|
|
11564
|
-
};
|
|
11565
|
-
}
|
|
11566
|
-
case "punct":
|
|
11567
|
-
if (tok.punct === "(") {
|
|
11568
|
-
const inner = this.parseTernary();
|
|
11569
|
-
this.expectPunct(")");
|
|
11570
|
-
return inner;
|
|
11571
|
-
}
|
|
11572
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
11573
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
11574
|
-
}
|
|
11575
|
-
}
|
|
11576
|
-
parseCall(callee, pos) {
|
|
11577
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
11578
|
-
this.expectPunct("(");
|
|
11579
|
-
const args = [];
|
|
11580
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
11581
|
-
args.push(this.parseTernary());
|
|
11582
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
11583
|
-
if (this.matchPunct(",")) continue;
|
|
11584
|
-
this.expectPunct(")");
|
|
11585
|
-
break;
|
|
11586
|
-
}
|
|
11587
|
-
this.callees.add(callee);
|
|
11588
|
-
this.countNode();
|
|
11589
|
-
return {
|
|
11590
|
-
kind: "call",
|
|
11591
|
-
callee,
|
|
11592
|
-
args
|
|
11593
|
-
};
|
|
11594
|
-
}
|
|
11595
|
-
};
|
|
11596
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
11597
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
11598
|
-
function parseExpression(source) {
|
|
11599
|
-
return new Parser(tokenize(source)).parse();
|
|
11600
|
-
}
|
|
11601
|
-
/**
|
|
11602
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
11603
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
11604
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
11605
|
-
* one per read on a hot resolve path.
|
|
11606
|
-
*
|
|
11607
|
-
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
11608
|
-
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
11609
|
-
* callers is safe and maximises hit rate.
|
|
11610
|
-
*/
|
|
11611
|
-
var cache = /* @__PURE__ */ new Map();
|
|
11612
|
-
function getCached(source) {
|
|
11613
|
-
const hit = cache.get(source);
|
|
11614
|
-
if (hit !== void 0) {
|
|
11615
|
-
cache.delete(source);
|
|
11616
|
-
cache.set(source, hit);
|
|
11617
|
-
return hit;
|
|
11618
|
-
}
|
|
11619
|
-
let result;
|
|
11620
|
-
try {
|
|
11621
|
-
result = {
|
|
11622
|
-
ok: true,
|
|
11623
|
-
parsed: parseExpression(source)
|
|
11624
|
-
};
|
|
11625
|
-
} catch (err) {
|
|
11626
|
-
result = {
|
|
11627
|
-
ok: false,
|
|
11628
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
11629
|
-
};
|
|
11630
|
-
}
|
|
11631
|
-
cache.set(source, result);
|
|
11632
|
-
if (cache.size > 256) {
|
|
11633
|
-
const oldest = cache.keys().next().value;
|
|
11634
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
11635
|
-
}
|
|
11636
|
-
return result;
|
|
11637
|
-
}
|
|
11638
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
11639
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
11640
|
-
function compileExpressionSafe(source) {
|
|
11641
|
-
return getCached(source);
|
|
11642
|
-
}
|
|
11643
|
-
Object.freeze({});
|
|
11644
|
-
/**
|
|
11645
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
11646
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
11647
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
11648
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
11649
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
11650
|
-
*/
|
|
11651
|
-
function validateExpressionSource(src) {
|
|
11652
|
-
const names = Object.keys(src.bindings);
|
|
11653
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
11654
|
-
for (const name of names) {
|
|
11655
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
11656
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
11657
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
11658
|
-
}
|
|
11659
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
11660
|
-
if (!compiled.ok) return compiled.error;
|
|
11661
|
-
const bound = new Set(names);
|
|
11662
|
-
for (const id of compiled.parsed.identifiers) {
|
|
11663
|
-
if (id === "now") continue;
|
|
11664
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
11665
|
-
}
|
|
11666
|
-
return null;
|
|
11667
|
-
}
|
|
11668
11170
|
var ProviderStatusSchema = object({
|
|
11669
11171
|
connected: boolean(),
|
|
11670
11172
|
deviceCount: number(),
|
|
@@ -11789,92 +11291,6 @@ var ChildLayoutEntrySchema = object({
|
|
|
11789
11291
|
order: number().optional(),
|
|
11790
11292
|
collapsed: boolean().optional()
|
|
11791
11293
|
});
|
|
11792
|
-
/** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
|
|
11793
|
-
* `device-management.ts`. Source is a union: a FIELD source copies a sibling
|
|
11794
|
-
* accessory's status field (`kind` optional/absent for wire compat); a
|
|
11795
|
-
* LITERAL source carries a per-device constant (no sibling is read); a
|
|
11796
|
-
* GLOBAL source (P2e) copies ANY device's status field, addressed by the
|
|
11797
|
-
* source device's full re-sync-stable `stableId`. */
|
|
11798
|
-
var DeviceLinkFieldSourceSchema = object({
|
|
11799
|
-
kind: literal("field").optional(),
|
|
11800
|
-
sourceKey: string(),
|
|
11801
|
-
cap: string(),
|
|
11802
|
-
fieldPath: string()
|
|
11803
|
-
});
|
|
11804
|
-
var DeviceLinkLiteralSourceSchema = object({
|
|
11805
|
-
kind: literal("literal"),
|
|
11806
|
-
value: union([
|
|
11807
|
-
string(),
|
|
11808
|
-
number(),
|
|
11809
|
-
boolean(),
|
|
11810
|
-
_null()
|
|
11811
|
-
])
|
|
11812
|
-
});
|
|
11813
|
-
var DeviceLinkGlobalSourceSchema = object({
|
|
11814
|
-
kind: literal("global"),
|
|
11815
|
-
sourceStableId: string(),
|
|
11816
|
-
cap: string(),
|
|
11817
|
-
fieldPath: string()
|
|
11818
|
-
});
|
|
11819
|
-
/** Expression source (Stage X): compute the target field from N named bindings
|
|
11820
|
-
* via the safe expression engine. Bindings are field | literal | global — never
|
|
11821
|
-
* another expression (no nesting). The `superRefine` runs the SAME author-time
|
|
11822
|
-
* validation as `validateExpressionSource` (compiles the expr, checks binding
|
|
11823
|
-
* names + identifier coverage) so every wire boundary that parses a DeviceLink
|
|
11824
|
-
* (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
|
|
11825
|
-
* Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
|
|
11826
|
-
var DeviceLinkExpressionSourceSchema = object({
|
|
11827
|
-
kind: literal("expression"),
|
|
11828
|
-
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
11829
|
-
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
|
|
11830
|
-
DeviceLinkFieldSourceSchema,
|
|
11831
|
-
DeviceLinkLiteralSourceSchema,
|
|
11832
|
-
DeviceLinkGlobalSourceSchema
|
|
11833
|
-
]))
|
|
11834
|
-
}).superRefine((src, ctx) => {
|
|
11835
|
-
const err = validateExpressionSource(src);
|
|
11836
|
-
if (err !== null) ctx.addIssue({
|
|
11837
|
-
code: "custom",
|
|
11838
|
-
message: err,
|
|
11839
|
-
path: ["expr"]
|
|
11840
|
-
});
|
|
11841
|
-
});
|
|
11842
|
-
var DeviceLinkSchema = object({
|
|
11843
|
-
id: string(),
|
|
11844
|
-
source: union([
|
|
11845
|
-
DeviceLinkFieldSourceSchema,
|
|
11846
|
-
DeviceLinkLiteralSourceSchema,
|
|
11847
|
-
DeviceLinkGlobalSourceSchema,
|
|
11848
|
-
DeviceLinkExpressionSourceSchema
|
|
11849
|
-
]),
|
|
11850
|
-
target: object({
|
|
11851
|
-
cap: string(),
|
|
11852
|
-
fieldPath: string(),
|
|
11853
|
-
itemKey: string().optional()
|
|
11854
|
-
}),
|
|
11855
|
-
transform: discriminatedUnion("kind", [
|
|
11856
|
-
object({ kind: literal("identity") }),
|
|
11857
|
-
object({
|
|
11858
|
-
kind: literal("enum-map"),
|
|
11859
|
-
mapping: record(string(), union([
|
|
11860
|
-
string(),
|
|
11861
|
-
number(),
|
|
11862
|
-
boolean()
|
|
11863
|
-
])),
|
|
11864
|
-
fallback: union([
|
|
11865
|
-
string(),
|
|
11866
|
-
number(),
|
|
11867
|
-
boolean()
|
|
11868
|
-
]).optional()
|
|
11869
|
-
}),
|
|
11870
|
-
object({
|
|
11871
|
-
kind: literal("linear"),
|
|
11872
|
-
scale: number(),
|
|
11873
|
-
offset: number(),
|
|
11874
|
-
clamp: tuple([number(), number()]).readonly().optional()
|
|
11875
|
-
})
|
|
11876
|
-
]).optional()
|
|
11877
|
-
});
|
|
11878
11294
|
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
11879
11295
|
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
11880
11296
|
var DeviceCapDisplayOverrideSchema = object({
|
|
@@ -11954,8 +11370,6 @@ var DeviceInfoSchema = object({
|
|
|
11954
11370
|
* named accordion sections (with optional intra-section order). See
|
|
11955
11371
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
11956
11372
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
11957
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
11958
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
11959
11373
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
11960
11374
|
display: DeviceDisplayOverrideSchema.optional()
|
|
11961
11375
|
});
|
|
@@ -11964,7 +11378,7 @@ var ConfigEntrySchema = object({
|
|
|
11964
11378
|
value: unknown(),
|
|
11965
11379
|
description: string().optional()
|
|
11966
11380
|
});
|
|
11967
|
-
var
|
|
11381
|
+
var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
|
|
11968
11382
|
/** One resolved linked device — the compact projection consumers need. */
|
|
11969
11383
|
var LinkedDeviceSchema = object({
|
|
11970
11384
|
deviceId: number(),
|
|
@@ -12027,8 +11441,6 @@ var DeviceMetaSchema = object({
|
|
|
12027
11441
|
* accordion sections (with optional intra-section order). See
|
|
12028
11442
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
12029
11443
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
12030
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
12031
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
12032
11444
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
12033
11445
|
* Optional: only present for accessory children that carry a known role. */
|
|
12034
11446
|
role: string().nullable().optional(),
|
|
@@ -12121,12 +11533,6 @@ method(object({
|
|
|
12121
11533
|
}), _void(), {
|
|
12122
11534
|
kind: "mutation",
|
|
12123
11535
|
auth: "admin"
|
|
12124
|
-
}), method(object({
|
|
12125
|
-
deviceId: number(),
|
|
12126
|
-
deviceLinks: array(DeviceLinkSchema).readonly()
|
|
12127
|
-
}), _void(), {
|
|
12128
|
-
kind: "mutation",
|
|
12129
|
-
auth: "admin"
|
|
12130
11536
|
}), method(object({
|
|
12131
11537
|
deviceId: number(),
|
|
12132
11538
|
display: DeviceDisplayOverrideSchema.nullable()
|
|
@@ -12208,7 +11614,7 @@ method(object({
|
|
|
12208
11614
|
* shipping 293 rows to find 12. */
|
|
12209
11615
|
isCamera: boolean().optional()
|
|
12210
11616
|
}), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
|
|
12211
|
-
mode:
|
|
11617
|
+
mode: LinkedDevicesModeSchema,
|
|
12212
11618
|
devices: array(LinkedDeviceSchema)
|
|
12213
11619
|
})), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
12214
11620
|
deviceId: number(),
|
|
@@ -12241,11 +11647,7 @@ method(object({
|
|
|
12241
11647
|
deviceId: number(),
|
|
12242
11648
|
entries: array(object({
|
|
12243
11649
|
capName: string(),
|
|
12244
|
-
kind: _enum([
|
|
12245
|
-
"native",
|
|
12246
|
-
"wrapped",
|
|
12247
|
-
"linked"
|
|
12248
|
-
]),
|
|
11650
|
+
kind: _enum(["native", "wrapped"]),
|
|
12249
11651
|
providerAddonId: string(),
|
|
12250
11652
|
providerNodeId: string(),
|
|
12251
11653
|
nativeAddonId: string()
|
|
@@ -12254,11 +11656,7 @@ method(object({
|
|
|
12254
11656
|
deviceId: number(),
|
|
12255
11657
|
entries: array(object({
|
|
12256
11658
|
capName: string(),
|
|
12257
|
-
kind: _enum([
|
|
12258
|
-
"native",
|
|
12259
|
-
"wrapped",
|
|
12260
|
-
"linked"
|
|
12261
|
-
]),
|
|
11659
|
+
kind: _enum(["native", "wrapped"]),
|
|
12262
11660
|
providerAddonId: string(),
|
|
12263
11661
|
providerNodeId: string(),
|
|
12264
11662
|
nativeAddonId: string()
|
|
@@ -13061,7 +12459,7 @@ var MotionAnalysisResultSchema = object({
|
|
|
13061
12459
|
frameHeight: number(),
|
|
13062
12460
|
analysisMs: number()
|
|
13063
12461
|
});
|
|
13064
|
-
method(object({
|
|
12462
|
+
DeviceType.Camera, method(object({
|
|
13065
12463
|
deviceId: number(),
|
|
13066
12464
|
frame: FrameInputSchema.optional(),
|
|
13067
12465
|
frameHandle: FrameHandleSchema.optional()
|
|
@@ -15250,6 +14648,18 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15250
14648
|
* redirect_uri that does not start with one of these. Required —
|
|
15251
14649
|
* an empty list means the integration can never complete linking. */
|
|
15252
14650
|
allowedRedirectPrefixes: array(string()).min(1),
|
|
14651
|
+
/** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
|
|
14652
|
+
* RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
|
|
14653
|
+
* `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
|
|
14654
|
+
* whose address the hub cannot know in advance (a Home Assistant at
|
|
14655
|
+
* `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
|
|
14656
|
+
* exactly; a public host never satisfies this branch, so it is not a
|
|
14657
|
+
* wildcard prefix by another name. */
|
|
14658
|
+
allowedPrivateHostPaths: array(string()).optional(),
|
|
14659
|
+
/** When true this is a PUBLIC client (source is published, no secret can be
|
|
14660
|
+
* protected) and PKCE is mandatory: `/authorize` refuses without an S256
|
|
14661
|
+
* `code_challenge`, `/token` refuses without the matching `code_verifier`. */
|
|
14662
|
+
requiresPkce: boolean().optional(),
|
|
15253
14663
|
/** Optional public origin (no trailing slash) that this integration's
|
|
15254
14664
|
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
15255
14665
|
* operator-selected external-access endpoint resolved by the addon. When
|
|
@@ -15400,7 +14810,7 @@ var TrackEnvelopeSchema = object({
|
|
|
15400
14810
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
15401
14811
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
15402
14812
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
15403
|
-
* zonesVisited, bestEventId, envelope) and returns `positions` /
|
|
14813
|
+
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
15404
14814
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
15405
14815
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
15406
14816
|
* (`getObjectEvents` et al.).
|
|
@@ -15520,6 +14930,60 @@ var TrackFlagsSchema = object({
|
|
|
15520
14930
|
* `trained` without a re-fetch. */
|
|
15521
14931
|
retrainStatus: RetrainStatusSchema
|
|
15522
14932
|
});
|
|
14933
|
+
union([literal(1), literal(2)]);
|
|
14934
|
+
/**
|
|
14935
|
+
* WHO decided a label, and when. Carried per tier so a value can be traced to
|
|
14936
|
+
* the step and model that produced it — which is what makes the write rule
|
|
14937
|
+
* arguable after the fact ("why is 592's label `dog` and not `Canis lupus`?")
|
|
14938
|
+
* and what lets a migrated, UNATTRIBUTED value be told apart from a real one.
|
|
14939
|
+
*
|
|
14940
|
+
* `stepId` is the pipeline step id (`animal-classifier`, `bird-classifier`,
|
|
14941
|
+
* `plate-ocr`, `face-embedding`, `object-detection`), or the sentinel
|
|
14942
|
+
* `migration:4g` for a value the 4g migration moved from the single-slot era —
|
|
14943
|
+
* that value has no provenance, and the write rule lets ANY properly-attributed
|
|
14944
|
+
* write of the same tier replace it regardless of score.
|
|
14945
|
+
*/
|
|
14946
|
+
var LabelAttributionSchema = object({
|
|
14947
|
+
stepId: string(),
|
|
14948
|
+
modelId: string().optional(),
|
|
14949
|
+
decidedAt: number()
|
|
14950
|
+
});
|
|
14951
|
+
/**
|
|
14952
|
+
* The TIERED label model (roadmap 4g), spread into `TrackSchema` and
|
|
14953
|
+
* `ObjectEventSchema` from ONE place so the two surfaces cannot drift — a
|
|
14954
|
+
* track and its events always answer the same question the same way.
|
|
14955
|
+
*
|
|
14956
|
+
* Two scalar columns, not an array: every consumer wants "the coarse one" or
|
|
14957
|
+
* "the fine one", and an array made both a scan. `label` is tier 1, `subLabel`
|
|
14958
|
+
* is tier 2, and each carries its own score + attribution.
|
|
14959
|
+
*
|
|
14960
|
+
* **Reading it.** What a human should be shown is `subLabel ?? label` — the
|
|
14961
|
+
* finest thing known. Before 4g the single `label` column held the finest
|
|
14962
|
+
* value, so a consumer that has not been updated reads the tier-1 slot and
|
|
14963
|
+
* shows nothing on a species-only row; that is why the migration puts every
|
|
14964
|
+
* pre-4g value in tier 2 (it cannot regress a display that reads the fallback)
|
|
14965
|
+
* and why the read surfaces were changed in the same train.
|
|
14966
|
+
*
|
|
14967
|
+
* **Writing it.** The slots are independent, which is the whole point: a
|
|
14968
|
+
* tier-1 write (`bird`) can never overwrite a tier-2 value (`Turdus
|
|
14969
|
+
* migratorius`), so fineness cannot regress by construction. Within a tier the
|
|
14970
|
+
* higher score wins. One rule, one implementation — see
|
|
14971
|
+
* `pipeline/label-tier.ts` in addon-post-analysis.
|
|
14972
|
+
*/
|
|
14973
|
+
var TieredLabelFields = {
|
|
14974
|
+
/** Tier 1 — the sub-class. See {@link LabelTierSchema}. */
|
|
14975
|
+
label: string().optional(),
|
|
14976
|
+
/** Confidence of the tier-1 value, as reported by the deciding step. */
|
|
14977
|
+
labelScore: number().optional(),
|
|
14978
|
+
/** Provenance of the tier-1 value. See {@link LabelAttributionSchema}. */
|
|
14979
|
+
labelMeta: LabelAttributionSchema.optional(),
|
|
14980
|
+
/** Tier 2 — the instance. See {@link LabelTierSchema}. */
|
|
14981
|
+
subLabel: string().optional(),
|
|
14982
|
+
/** Confidence of the tier-2 value, as reported by the deciding step. */
|
|
14983
|
+
subLabelScore: number().optional(),
|
|
14984
|
+
/** Provenance of the tier-2 value. See {@link LabelAttributionSchema}. */
|
|
14985
|
+
subLabelMeta: LabelAttributionSchema.optional()
|
|
14986
|
+
};
|
|
15523
14987
|
/** Per-camera slice of a training-export estimate. */
|
|
15524
14988
|
var TrainingExportDeviceTotalsSchema = object({
|
|
15525
14989
|
deviceId: number(),
|
|
@@ -15544,7 +15008,7 @@ var TrackSchema = object({
|
|
|
15544
15008
|
trackId: string(),
|
|
15545
15009
|
deviceId: number(),
|
|
15546
15010
|
className: string(),
|
|
15547
|
-
|
|
15011
|
+
...TieredLabelFields,
|
|
15548
15012
|
producingDeviceName: string().optional(),
|
|
15549
15013
|
/** Track provenance. Absent ⇒ `pipeline` (legacy rows). */
|
|
15550
15014
|
source: TrackSourceSchema.optional(),
|
|
@@ -15583,6 +15047,24 @@ var TrackSchema = object({
|
|
|
15583
15047
|
* Populated from the persisted envelope columns on historical reads;
|
|
15584
15048
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
15585
15049
|
envelope: TrackEnvelopeSchema.optional(),
|
|
15050
|
+
/**
|
|
15051
|
+
* A face DETECTOR found a face on this track — nothing more. It says the
|
|
15052
|
+
* detail plane produced a `face` detail; it does NOT say the face was
|
|
15053
|
+
* embedded, matched, above `minFacePx`, or that the recognizer was even
|
|
15054
|
+
* enabled. Set once and never cleared.
|
|
15055
|
+
*
|
|
15056
|
+
* **This exists so "face present but not recognised" is expressible.** A
|
|
15057
|
+
* recognised identity lands in `subLabel` (attributed to the face chain via
|
|
15058
|
+
* `subLabelMeta.stepId`), so before this field a track with an unmatched face
|
|
15059
|
+
* and a track with no face at all were byte-identical on the wire and no
|
|
15060
|
+
* surface could tell them apart. The read is `hasFace === true && subLabel
|
|
15061
|
+
* === undefined`.
|
|
15062
|
+
*
|
|
15063
|
+
* **Absent ≠ false.** Every row written before the column existed omits it,
|
|
15064
|
+
* and so does every server that predates the field — a consumer must test
|
|
15065
|
+
* `=== true` and render nothing otherwise, never infer "no face".
|
|
15066
|
+
*/
|
|
15067
|
+
hasFace: boolean().optional(),
|
|
15586
15068
|
...TrackFlagFields,
|
|
15587
15069
|
...TrackRetrainFields
|
|
15588
15070
|
});
|
|
@@ -15657,7 +15139,7 @@ var ObjectEventSchema = object({
|
|
|
15657
15139
|
/** Omitted in slim projection. */
|
|
15658
15140
|
trackId: string().optional(),
|
|
15659
15141
|
className: string(),
|
|
15660
|
-
|
|
15142
|
+
...TieredLabelFields,
|
|
15661
15143
|
/** Omitted in slim projection. */
|
|
15662
15144
|
confidence: number().optional(),
|
|
15663
15145
|
/** Heavy JSON — omitted in slim projection. */
|
|
@@ -15738,6 +15220,173 @@ var MediaFileSchema = object({
|
|
|
15738
15220
|
* stored blob and a `?variant=thumb` rendering without fetching either.
|
|
15739
15221
|
*/
|
|
15740
15222
|
var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
|
|
15223
|
+
/**
|
|
15224
|
+
* The MACRO tier of an annotation — a CLOSED set.
|
|
15225
|
+
*
|
|
15226
|
+
* This is what the exported detector predicts, so a typo here is a new class
|
|
15227
|
+
* with one example in it. `label` and `subLabel` are open strings by contrast:
|
|
15228
|
+
* the whole point of the page is teaching the model things it does not know
|
|
15229
|
+
* yet, and constraining that vocabulary would make it useless.
|
|
15230
|
+
*
|
|
15231
|
+
* A macro class is NEVER a label. The provider refuses a write whose `label` or
|
|
15232
|
+
* `subLabel` is one of these values, in any casing, because once `person`
|
|
15233
|
+
* exists in both tiers "every person box" stops being answerable without
|
|
15234
|
+
* knowing every string anyone ever typed — and the damage is retroactive.
|
|
15235
|
+
*/
|
|
15236
|
+
var RetrainMacroClassSchema = _enum([
|
|
15237
|
+
"person",
|
|
15238
|
+
"vehicle",
|
|
15239
|
+
"animal",
|
|
15240
|
+
"package",
|
|
15241
|
+
"face",
|
|
15242
|
+
"plate"
|
|
15243
|
+
]);
|
|
15244
|
+
/** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
|
|
15245
|
+
var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
|
|
15246
|
+
/** Did a human draw this box, or did the assist propose it? */
|
|
15247
|
+
var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
|
|
15248
|
+
/** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
|
|
15249
|
+
var RetrainBboxSchema = object({
|
|
15250
|
+
x: number(),
|
|
15251
|
+
y: number(),
|
|
15252
|
+
w: number(),
|
|
15253
|
+
h: number()
|
|
15254
|
+
});
|
|
15255
|
+
/**
|
|
15256
|
+
* One annotated subject.
|
|
15257
|
+
*
|
|
15258
|
+
* `bbox` is normalised against the full frame, ALWAYS. The per-model shapes
|
|
15259
|
+
* (letterboxed root / zone-cropped package / subject-cropped classifier) are
|
|
15260
|
+
* derived from it at export and never stored — storing them is how one feature
|
|
15261
|
+
* space ends up holding two crops of the same subject (D52).
|
|
15262
|
+
*/
|
|
15263
|
+
var RetrainAnnotationSchema = object({
|
|
15264
|
+
id: string(),
|
|
15265
|
+
trackId: string(),
|
|
15266
|
+
deviceId: number(),
|
|
15267
|
+
/** The COPY in retrain storage — never the source track's media key. */
|
|
15268
|
+
mediaKey: string(),
|
|
15269
|
+
bbox: RetrainBboxSchema,
|
|
15270
|
+
macroClass: RetrainMacroClassSchema,
|
|
15271
|
+
label: string().optional(),
|
|
15272
|
+
subLabel: string().optional(),
|
|
15273
|
+
kind: RetrainAnnotationKindSchema,
|
|
15274
|
+
source: RetrainAnnotationSourceSchema,
|
|
15275
|
+
/** Which model proposed this box — or, on a `model_error`, drew the phantom. */
|
|
15276
|
+
assistModelId: string().optional(),
|
|
15277
|
+
assistScore: number().optional(),
|
|
15278
|
+
exportedInBatch: string().optional(),
|
|
15279
|
+
createdAt: number()
|
|
15280
|
+
});
|
|
15281
|
+
/** The write form — the server owns `id`, `createdAt` and the frame binding. */
|
|
15282
|
+
var RetrainAnnotationDraftSchema = RetrainAnnotationSchema.omit({
|
|
15283
|
+
id: true,
|
|
15284
|
+
trackId: true,
|
|
15285
|
+
deviceId: true,
|
|
15286
|
+
mediaKey: true,
|
|
15287
|
+
createdAt: true,
|
|
15288
|
+
exportedInBatch: true
|
|
15289
|
+
});
|
|
15290
|
+
/** A track sitting in `staging`, with everything the worklist needs to rank it. */
|
|
15291
|
+
var RetrainTrackSchema = object({
|
|
15292
|
+
trackId: string(),
|
|
15293
|
+
deviceId: number(),
|
|
15294
|
+
className: string(),
|
|
15295
|
+
label: string().optional(),
|
|
15296
|
+
firstSeen: number(),
|
|
15297
|
+
lastSeen: number(),
|
|
15298
|
+
/** How many frames the dataset already holds from this track. */
|
|
15299
|
+
frameCount: number().int(),
|
|
15300
|
+
/** How many subjects have been annotated on those frames. `0` with
|
|
15301
|
+
* `frameCount: 0` is exactly "staging, still to work". */
|
|
15302
|
+
annotationCount: number().int()
|
|
15303
|
+
});
|
|
15304
|
+
/** A frame the picker may offer — an index row, no blob was read to produce it. */
|
|
15305
|
+
var RetrainFrameCandidateSchema = object({
|
|
15306
|
+
mediaKey: string(),
|
|
15307
|
+
kind: MediaFileKindEnum,
|
|
15308
|
+
timestamp: number(),
|
|
15309
|
+
sizeBytes: number().int(),
|
|
15310
|
+
/** A copy of this original already exists — selecting it is free and cannot
|
|
15311
|
+
* fail, whatever became of the original. */
|
|
15312
|
+
copied: boolean()
|
|
15313
|
+
});
|
|
15314
|
+
/** A frame the dataset OWNS: bytes copied at selection time. */
|
|
15315
|
+
var RetrainFrameSchema = object({
|
|
15316
|
+
frameId: string(),
|
|
15317
|
+
deviceId: number(),
|
|
15318
|
+
trackId: string(),
|
|
15319
|
+
/** Provenance only. It may already point at nothing — that is expected. */
|
|
15320
|
+
sourceMediaKey: string(),
|
|
15321
|
+
sourceKind: MediaFileKindEnum,
|
|
15322
|
+
sizeBytes: number().int(),
|
|
15323
|
+
width: number().int(),
|
|
15324
|
+
height: number().int(),
|
|
15325
|
+
copiedAt: number()
|
|
15326
|
+
});
|
|
15327
|
+
/** Why a copy-on-select could not be honoured — named, never a silent skip. */
|
|
15328
|
+
var RetrainCopyRefusalSchema = _enum([
|
|
15329
|
+
"source-missing",
|
|
15330
|
+
"unreadable-image",
|
|
15331
|
+
"write-failed"
|
|
15332
|
+
]);
|
|
15333
|
+
var RetrainFrameSelectionSchema = object({
|
|
15334
|
+
copied: array(RetrainFrameSchema).readonly(),
|
|
15335
|
+
refused: array(object({
|
|
15336
|
+
sourceMediaKey: string(),
|
|
15337
|
+
reason: RetrainCopyRefusalSchema
|
|
15338
|
+
})).readonly()
|
|
15339
|
+
});
|
|
15340
|
+
var RetrainFrameListSchema = object({
|
|
15341
|
+
candidates: array(RetrainFrameCandidateSchema).readonly(),
|
|
15342
|
+
copies: array(RetrainFrameSchema).readonly(),
|
|
15343
|
+
/** What the page pre-selects — the native key frame when one survives. */
|
|
15344
|
+
autoPickMediaKey: string().optional()
|
|
15345
|
+
});
|
|
15346
|
+
/** What the operator asked the assist to look for. */
|
|
15347
|
+
var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
|
|
15348
|
+
kind: literal("package"),
|
|
15349
|
+
zone: RetrainBboxSchema.optional()
|
|
15350
|
+
}), object({
|
|
15351
|
+
kind: literal("objects"),
|
|
15352
|
+
modelId: string(),
|
|
15353
|
+
minScore: number().optional()
|
|
15354
|
+
})]);
|
|
15355
|
+
/**
|
|
15356
|
+
* The assist's answer — a discriminated union, because "the model saw nothing"
|
|
15357
|
+
* and "this node cannot run that model" lead to different next moves and a
|
|
15358
|
+
* nullable result cannot tell them apart.
|
|
15359
|
+
*/
|
|
15360
|
+
var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
|
|
15361
|
+
kind: literal("proposed"),
|
|
15362
|
+
modelId: string(),
|
|
15363
|
+
stepId: string(),
|
|
15364
|
+
minScore: number(),
|
|
15365
|
+
/** Drafts, ready to edit. `source: 'assist'` until the operator touches one. */
|
|
15366
|
+
proposals: array(RetrainAnnotationDraftSchema).readonly(),
|
|
15367
|
+
/** Returned by the runner but removed by the threshold. */
|
|
15368
|
+
belowThreshold: number().int()
|
|
15369
|
+
}), object({
|
|
15370
|
+
kind: literal("refused"),
|
|
15371
|
+
/** `no-zone` is ours; the rest are the runner's own refusal vocabulary. */
|
|
15372
|
+
reason: string(),
|
|
15373
|
+
detail: string().optional()
|
|
15374
|
+
})]);
|
|
15375
|
+
/** The outcome of a lifecycle move owned by the retrain page. */
|
|
15376
|
+
var RetrainTransitionResultSchema = object({
|
|
15377
|
+
trackId: string(),
|
|
15378
|
+
/** Where the track ended up, whatever happened. */
|
|
15379
|
+
retrainStatus: RetrainStatusSchema,
|
|
15380
|
+
/** `false` ⇒ the move was refused or was a no-op; `reason` says which. */
|
|
15381
|
+
changed: boolean(),
|
|
15382
|
+
reason: _enum([
|
|
15383
|
+
"unknown-track",
|
|
15384
|
+
"no-frames-copied",
|
|
15385
|
+
"not-staging",
|
|
15386
|
+
"not-trained",
|
|
15387
|
+
"unchanged"
|
|
15388
|
+
]).optional()
|
|
15389
|
+
});
|
|
15741
15390
|
var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
|
|
15742
15391
|
var MAX_EVENT_QUERY_LIMIT = 5e3;
|
|
15743
15392
|
var DeviceEventQueryInput = object({
|
|
@@ -15792,7 +15441,7 @@ var KeyEventSchema = object({
|
|
|
15792
15441
|
/** Track start time (firstSeen). */
|
|
15793
15442
|
timestamp: number(),
|
|
15794
15443
|
className: string(),
|
|
15795
|
-
|
|
15444
|
+
...TieredLabelFields,
|
|
15796
15445
|
importance: number(),
|
|
15797
15446
|
/** Highest-confidence ObjectEvent id for the track (empty when none). */
|
|
15798
15447
|
bestEventId: string(),
|
|
@@ -16066,6 +15715,79 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
16066
15715
|
}), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
16067
15716
|
kind: "query",
|
|
16068
15717
|
auth: "admin"
|
|
15718
|
+
}), method(object({
|
|
15719
|
+
/** Empty ⇒ every camera that has staging tracks. A LIST, not a single
|
|
15720
|
+
* `deviceId`, deliberately: `deviceId` would make this device-bound and
|
|
15721
|
+
* route it at one camera's owner, and "every camera" would stop being
|
|
15722
|
+
* expressible at all. */
|
|
15723
|
+
deviceIds: array(number()).optional(),
|
|
15724
|
+
limit: number().int().min(1).max(500).optional()
|
|
15725
|
+
}), array(RetrainTrackSchema).readonly(), {
|
|
15726
|
+
kind: "query",
|
|
15727
|
+
auth: "admin"
|
|
15728
|
+
}), method(object({ trackId: string() }), RetrainFrameListSchema, {
|
|
15729
|
+
kind: "query",
|
|
15730
|
+
auth: "admin"
|
|
15731
|
+
}), method(object({
|
|
15732
|
+
deviceId: number(),
|
|
15733
|
+
trackId: string(),
|
|
15734
|
+
mediaKeys: array(string()).min(1)
|
|
15735
|
+
}), RetrainFrameSelectionSchema, {
|
|
15736
|
+
kind: "mutation",
|
|
15737
|
+
auth: "admin"
|
|
15738
|
+
}), method(object({
|
|
15739
|
+
deviceId: number(),
|
|
15740
|
+
trackId: string(),
|
|
15741
|
+
frameId: string()
|
|
15742
|
+
}), object({
|
|
15743
|
+
removed: boolean(),
|
|
15744
|
+
removedAnnotations: number().int()
|
|
15745
|
+
}), {
|
|
15746
|
+
kind: "mutation",
|
|
15747
|
+
auth: "admin"
|
|
15748
|
+
}), method(object({ frameId: string() }), object({
|
|
15749
|
+
base64: string(),
|
|
15750
|
+
width: number().int(),
|
|
15751
|
+
height: number().int()
|
|
15752
|
+
}), {
|
|
15753
|
+
kind: "query",
|
|
15754
|
+
auth: "admin"
|
|
15755
|
+
}), method(object({
|
|
15756
|
+
deviceId: number(),
|
|
15757
|
+
trackId: string(),
|
|
15758
|
+
frameId: string(),
|
|
15759
|
+
subject: RetrainAssistSubjectSchema,
|
|
15760
|
+
/** Which node runs it. Absent ⇒ wherever an unowned call lands. */
|
|
15761
|
+
nodeId: string().optional()
|
|
15762
|
+
}), RetrainAssistResultSchema, {
|
|
15763
|
+
kind: "mutation",
|
|
15764
|
+
auth: "admin"
|
|
15765
|
+
}), method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
|
|
15766
|
+
kind: "query",
|
|
15767
|
+
auth: "admin"
|
|
15768
|
+
}), method(object({
|
|
15769
|
+
deviceId: number(),
|
|
15770
|
+
trackId: string(),
|
|
15771
|
+
frameId: string(),
|
|
15772
|
+
annotations: array(RetrainAnnotationDraftSchema)
|
|
15773
|
+
}), array(RetrainAnnotationSchema).readonly(), {
|
|
15774
|
+
kind: "mutation",
|
|
15775
|
+
auth: "admin"
|
|
15776
|
+
}), method(object({
|
|
15777
|
+
deviceId: number(),
|
|
15778
|
+
trackId: string()
|
|
15779
|
+
}), RetrainTransitionResultSchema, {
|
|
15780
|
+
kind: "mutation",
|
|
15781
|
+
auth: "admin"
|
|
15782
|
+
}), method(object({
|
|
15783
|
+
deviceId: number(),
|
|
15784
|
+
trackId: string()
|
|
15785
|
+
}), RetrainTransitionResultSchema, {
|
|
15786
|
+
kind: "mutation",
|
|
15787
|
+
auth: "admin"
|
|
15788
|
+
}), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
15789
|
+
kind: "query",
|
|
15790
|
+
auth: "admin"
|
|
16069
15791
|
}), method(object({
|
|
16070
15792
|
eventId: string(),
|
|
16071
15793
|
kind: MediaFileKindEnum.optional()
|
|
@@ -16645,6 +16367,22 @@ var DetailResultSchema = object({
|
|
|
16645
16367
|
bbox: NativeCropBboxSchema.optional(),
|
|
16646
16368
|
embedding: string().optional(),
|
|
16647
16369
|
label: string().optional(),
|
|
16370
|
+
/**
|
|
16371
|
+
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
16372
|
+
* `StepDefinition.labelTier` (roadmap 4g). Present only when `label` is.
|
|
16373
|
+
*
|
|
16374
|
+
* It rides the wire rather than being resolved by the consumer because the
|
|
16375
|
+
* declaration lives with the step definition, which only the executing node
|
|
16376
|
+
* has: post-analysis holds no step registry, and re-deriving the tier from
|
|
16377
|
+
* `className` there would be exactly the inference this model exists to
|
|
16378
|
+
* forbid. A `label` that arrives WITHOUT this field is refused by the write
|
|
16379
|
+
* rule and logged (`label tier undeclared`) — an older runner therefore
|
|
16380
|
+
* stops enriching rather than guessing, which is why addon-pipeline is
|
|
16381
|
+
* deployed BEFORE addon-post-analysis.
|
|
16382
|
+
*/
|
|
16383
|
+
labelTier: union([literal(1), literal(2)]).optional(),
|
|
16384
|
+
/** Model that produced `label` — carried into the tier's attribution. */
|
|
16385
|
+
labelModelId: string().optional(),
|
|
16648
16386
|
alignedCropJpeg: string().optional(),
|
|
16649
16387
|
/** Face short side (px) measured on the NATIVE crop surface. The `bbox`
|
|
16650
16388
|
* above is detection-frame px (≈6× smaller on a 4K camera) — min-face-size
|
|
@@ -17941,6 +17679,10 @@ var SsoBridgeClaimsSchema = object({
|
|
|
17941
17679
|
integrationId: string().optional(),
|
|
17942
17680
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
17943
17681
|
jti: string().optional(),
|
|
17682
|
+
/** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
|
|
17683
|
+
* client. Its PRESENCE is what makes the verifier mandatory at exchange,
|
|
17684
|
+
* so the requirement travels with the code and not with mutable config. */
|
|
17685
|
+
codeChallenge: string().optional(),
|
|
17944
17686
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
17945
17687
|
* tokens so the verify path can check the session is not revoked. */
|
|
17946
17688
|
sessionId: string().optional()
|
|
@@ -18493,7 +18235,7 @@ var ClipPlaybackSchema = object({
|
|
|
18493
18235
|
playbackEndpoints: array(string()).optional(),
|
|
18494
18236
|
token: string().optional()
|
|
18495
18237
|
});
|
|
18496
|
-
method(object({
|
|
18238
|
+
DeviceType.Camera, method(object({
|
|
18497
18239
|
deviceId: number(),
|
|
18498
18240
|
since: number(),
|
|
18499
18241
|
until: number(),
|
|
@@ -23569,13 +23311,18 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
23569
23311
|
username: string(),
|
|
23570
23312
|
scopes: array(TokenScopeSchema),
|
|
23571
23313
|
redirectUri: string(),
|
|
23572
|
-
hubUrl: string()
|
|
23314
|
+
hubUrl: string(),
|
|
23315
|
+
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
23316
|
+
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
23317
|
+
codeChallenge: string().optional()
|
|
23573
23318
|
}), object({ code: string() }), {
|
|
23574
23319
|
kind: "mutation",
|
|
23575
23320
|
access: "create"
|
|
23576
23321
|
}), method(object({
|
|
23577
23322
|
code: string(),
|
|
23578
|
-
redirectUri: string()
|
|
23323
|
+
redirectUri: string(),
|
|
23324
|
+
/** PKCE verifier. REQUIRED when the code carries a challenge. */
|
|
23325
|
+
codeVerifier: string().optional()
|
|
23579
23326
|
}), object({
|
|
23580
23327
|
accessToken: string(),
|
|
23581
23328
|
refreshToken: string(),
|
|
@@ -23915,76 +23662,705 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapsho
|
|
|
23915
23662
|
className: string().optional()
|
|
23916
23663
|
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
|
|
23917
23664
|
/**
|
|
23918
|
-
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
23919
|
-
* cap so a single CRUD surface backs every consumer; each stage has
|
|
23920
|
-
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
23921
|
-
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
23665
|
+
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
23666
|
+
* cap so a single CRUD surface backs every consumer; each stage has
|
|
23667
|
+
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
23668
|
+
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
23669
|
+
*
|
|
23670
|
+
* Extend the enum here when a new gating consumer comes online (audio
|
|
23671
|
+
* gating, alert filtering, …) — no other surface needs to change.
|
|
23672
|
+
*/
|
|
23673
|
+
var ZoneRuleStageEnum = _enum([
|
|
23674
|
+
"motion",
|
|
23675
|
+
"detection",
|
|
23676
|
+
"package"
|
|
23677
|
+
]);
|
|
23678
|
+
DeviceType.Camera, method(object({
|
|
23679
|
+
deviceId: number(),
|
|
23680
|
+
stage: ZoneRuleStageEnum
|
|
23681
|
+
}), array(ZoneRuleSchema).readonly()), method(object({
|
|
23682
|
+
deviceId: number(),
|
|
23683
|
+
stage: ZoneRuleStageEnum,
|
|
23684
|
+
rules: array(ZoneRuleSchema).readonly()
|
|
23685
|
+
}), _void(), {
|
|
23686
|
+
kind: "mutation",
|
|
23687
|
+
auth: "admin"
|
|
23688
|
+
}), object({
|
|
23689
|
+
motion: array(ZoneRuleSchema).readonly(),
|
|
23690
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
23691
|
+
package: array(ZoneRuleSchema).readonly()
|
|
23692
|
+
});
|
|
23693
|
+
/**
|
|
23694
|
+
* Accessory device helpers — shared across drivers.
|
|
23695
|
+
*
|
|
23696
|
+
* Many vendor-specific drivers register accessory child devices on
|
|
23697
|
+
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
23698
|
+
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
23699
|
+
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
23700
|
+
* spawning, builds a name derived from the parent, and produces a
|
|
23701
|
+
* stableId tied to the parent so boot-restore can reconstruct the
|
|
23702
|
+
* relationship.
|
|
23703
|
+
*
|
|
23704
|
+
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
23705
|
+
* drivers may reasonably disagree on the right type for an accessory
|
|
23706
|
+
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
23707
|
+
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
23708
|
+
* one canonical mapping was over-prescriptive and added a layer of
|
|
23709
|
+
* indirection without saving meaningful code at call sites — the
|
|
23710
|
+
* driver knows its own hardware best.
|
|
23711
|
+
*/
|
|
23712
|
+
/**
|
|
23713
|
+
* Subset of `DeviceRole` values that drivers register as child
|
|
23714
|
+
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
23715
|
+
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
23716
|
+
* children, so the call site reads as
|
|
23717
|
+
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
23718
|
+
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
23719
|
+
* any role works, including non-accessory ones like Doorbell).
|
|
23720
|
+
*/
|
|
23721
|
+
var AccessoryKind = {
|
|
23722
|
+
Siren: DeviceRole.Siren,
|
|
23723
|
+
Floodlight: DeviceRole.Floodlight,
|
|
23724
|
+
Spotlight: DeviceRole.Spotlight,
|
|
23725
|
+
PirSensor: DeviceRole.PirSensor,
|
|
23726
|
+
Chime: DeviceRole.Chime,
|
|
23727
|
+
Autotrack: DeviceRole.Autotrack,
|
|
23728
|
+
Nightvision: DeviceRole.Nightvision,
|
|
23729
|
+
PrivacyMask: DeviceRole.PrivacyMask
|
|
23730
|
+
};
|
|
23731
|
+
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
23732
|
+
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;
|
|
23733
|
+
new Set(Object.values(DeviceType));
|
|
23734
|
+
DeviceFeature.BatteryOperated;
|
|
23735
|
+
/**
|
|
23736
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
23737
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
23738
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
23739
|
+
*/
|
|
23740
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
23741
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
23742
|
+
var ExpressionParseError = class extends Error {
|
|
23743
|
+
position;
|
|
23744
|
+
constructor(message, position) {
|
|
23745
|
+
super(message);
|
|
23746
|
+
this.name = "ExpressionParseError";
|
|
23747
|
+
this.position = position;
|
|
23748
|
+
}
|
|
23749
|
+
};
|
|
23750
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
23751
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
23752
|
+
var ExpressionEvalError = class extends Error {
|
|
23753
|
+
constructor(message) {
|
|
23754
|
+
super(message);
|
|
23755
|
+
this.name = "ExpressionEvalError";
|
|
23756
|
+
}
|
|
23757
|
+
};
|
|
23758
|
+
/**
|
|
23759
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
23760
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
23761
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
23762
|
+
* own-property check against it.
|
|
23763
|
+
*
|
|
23764
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
23765
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
23766
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
23767
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
23768
|
+
* callable — they are simply "unknown function" at parse time.
|
|
23769
|
+
*
|
|
23770
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
23771
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
23772
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
23773
|
+
* closed rather than emitting a garbage value.
|
|
23774
|
+
*/
|
|
23775
|
+
function asFiniteNumber(value, name, index) {
|
|
23776
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
23777
|
+
return value;
|
|
23778
|
+
}
|
|
23779
|
+
function asString$1(value, name, index) {
|
|
23780
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
23781
|
+
return value;
|
|
23782
|
+
}
|
|
23783
|
+
function finiteResult(value, name) {
|
|
23784
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
23785
|
+
return value;
|
|
23786
|
+
}
|
|
23787
|
+
function allFiniteNumbers(args, name) {
|
|
23788
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
23789
|
+
}
|
|
23790
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
23791
|
+
var table = {
|
|
23792
|
+
min: {
|
|
23793
|
+
minArgs: 1,
|
|
23794
|
+
maxArgs: INF,
|
|
23795
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
23796
|
+
},
|
|
23797
|
+
max: {
|
|
23798
|
+
minArgs: 1,
|
|
23799
|
+
maxArgs: INF,
|
|
23800
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
23801
|
+
},
|
|
23802
|
+
abs: {
|
|
23803
|
+
minArgs: 1,
|
|
23804
|
+
maxArgs: 1,
|
|
23805
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
23806
|
+
},
|
|
23807
|
+
floor: {
|
|
23808
|
+
minArgs: 1,
|
|
23809
|
+
maxArgs: 1,
|
|
23810
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
23811
|
+
},
|
|
23812
|
+
ceil: {
|
|
23813
|
+
minArgs: 1,
|
|
23814
|
+
maxArgs: 1,
|
|
23815
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
23816
|
+
},
|
|
23817
|
+
sqrt: {
|
|
23818
|
+
minArgs: 1,
|
|
23819
|
+
maxArgs: 1,
|
|
23820
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
23821
|
+
},
|
|
23822
|
+
round: {
|
|
23823
|
+
minArgs: 1,
|
|
23824
|
+
maxArgs: 2,
|
|
23825
|
+
apply: (args) => {
|
|
23826
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
23827
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
23828
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
23829
|
+
const factor = 10 ** digits;
|
|
23830
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
23831
|
+
}
|
|
23832
|
+
},
|
|
23833
|
+
pow: {
|
|
23834
|
+
minArgs: 2,
|
|
23835
|
+
maxArgs: 2,
|
|
23836
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
23837
|
+
},
|
|
23838
|
+
clamp: {
|
|
23839
|
+
minArgs: 3,
|
|
23840
|
+
maxArgs: 3,
|
|
23841
|
+
apply: (args) => {
|
|
23842
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
23843
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
23844
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
23845
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
23846
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
23847
|
+
}
|
|
23848
|
+
},
|
|
23849
|
+
avg: {
|
|
23850
|
+
minArgs: 1,
|
|
23851
|
+
maxArgs: INF,
|
|
23852
|
+
apply: (args) => {
|
|
23853
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
23854
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
23855
|
+
}
|
|
23856
|
+
},
|
|
23857
|
+
sum: {
|
|
23858
|
+
minArgs: 1,
|
|
23859
|
+
maxArgs: INF,
|
|
23860
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
23861
|
+
},
|
|
23862
|
+
coalesce: {
|
|
23863
|
+
minArgs: 1,
|
|
23864
|
+
maxArgs: INF,
|
|
23865
|
+
apply: (args) => {
|
|
23866
|
+
for (const a of args) if (a !== null) return a;
|
|
23867
|
+
return null;
|
|
23868
|
+
}
|
|
23869
|
+
},
|
|
23870
|
+
age: {
|
|
23871
|
+
minArgs: 2,
|
|
23872
|
+
maxArgs: 2,
|
|
23873
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
23874
|
+
},
|
|
23875
|
+
convert: {
|
|
23876
|
+
minArgs: 3,
|
|
23877
|
+
maxArgs: 3,
|
|
23878
|
+
apply: (args, hooks) => {
|
|
23879
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
23880
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
23881
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
23882
|
+
if (hooks.convert) {
|
|
23883
|
+
const out = hooks.convert(x, from, to);
|
|
23884
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
23885
|
+
return finiteResult(out, "convert");
|
|
23886
|
+
}
|
|
23887
|
+
if (from === to) return x;
|
|
23888
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
23889
|
+
}
|
|
23890
|
+
}
|
|
23891
|
+
};
|
|
23892
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
23893
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
23894
|
+
* callees at parse time (immediate author feedback). */
|
|
23895
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
23896
|
+
/**
|
|
23897
|
+
* Resource-bound constants for the safe expression engine.
|
|
23898
|
+
*
|
|
23899
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
23900
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
23901
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
23902
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
23903
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
23904
|
+
*/
|
|
23905
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
23906
|
+
* rejected without allocation. */
|
|
23907
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
23908
|
+
/** A legal binding / identifier name. */
|
|
23909
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23910
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
23911
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
23912
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
23913
|
+
"now",
|
|
23914
|
+
"true",
|
|
23915
|
+
"false",
|
|
23916
|
+
"null"
|
|
23917
|
+
]);
|
|
23918
|
+
/**
|
|
23919
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
23920
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
23921
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
23922
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
23923
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
23924
|
+
* is a parse error with a source position, so member access / assignment /
|
|
23925
|
+
* template literals are lexically impossible.
|
|
23926
|
+
*/
|
|
23927
|
+
var KEYWORDS = new Set([
|
|
23928
|
+
"true",
|
|
23929
|
+
"false",
|
|
23930
|
+
"null"
|
|
23931
|
+
]);
|
|
23932
|
+
function isDigit(ch) {
|
|
23933
|
+
return ch >= "0" && ch <= "9";
|
|
23934
|
+
}
|
|
23935
|
+
function isIdentStart(ch) {
|
|
23936
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
23937
|
+
}
|
|
23938
|
+
function isIdentPart(ch) {
|
|
23939
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
23940
|
+
}
|
|
23941
|
+
function isWhitespace(ch) {
|
|
23942
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
23943
|
+
}
|
|
23944
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
23945
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
23946
|
+
* string. */
|
|
23947
|
+
function tokenize(source) {
|
|
23948
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
23949
|
+
const tokens = [];
|
|
23950
|
+
let i = 0;
|
|
23951
|
+
const n = source.length;
|
|
23952
|
+
while (i < n) {
|
|
23953
|
+
const ch = source[i];
|
|
23954
|
+
if (isWhitespace(ch)) {
|
|
23955
|
+
i += 1;
|
|
23956
|
+
continue;
|
|
23957
|
+
}
|
|
23958
|
+
if (isDigit(ch)) {
|
|
23959
|
+
const start = i;
|
|
23960
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
23961
|
+
if (i < n && source[i] === ".") {
|
|
23962
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
23963
|
+
i += 1;
|
|
23964
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
23965
|
+
}
|
|
23966
|
+
const text = source.slice(start, i);
|
|
23967
|
+
const value = Number(text);
|
|
23968
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
23969
|
+
tokens.push({
|
|
23970
|
+
type: "number",
|
|
23971
|
+
value,
|
|
23972
|
+
pos: start
|
|
23973
|
+
});
|
|
23974
|
+
continue;
|
|
23975
|
+
}
|
|
23976
|
+
if (ch === "'" || ch === "\"") {
|
|
23977
|
+
const quote = ch;
|
|
23978
|
+
const start = i;
|
|
23979
|
+
i += 1;
|
|
23980
|
+
let out = "";
|
|
23981
|
+
let closed = false;
|
|
23982
|
+
while (i < n) {
|
|
23983
|
+
const c = source[i];
|
|
23984
|
+
if (c === "\\") {
|
|
23985
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
23986
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
23987
|
+
out += next;
|
|
23988
|
+
i += 2;
|
|
23989
|
+
continue;
|
|
23990
|
+
}
|
|
23991
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
23992
|
+
}
|
|
23993
|
+
if (c === quote) {
|
|
23994
|
+
closed = true;
|
|
23995
|
+
i += 1;
|
|
23996
|
+
break;
|
|
23997
|
+
}
|
|
23998
|
+
out += c;
|
|
23999
|
+
i += 1;
|
|
24000
|
+
}
|
|
24001
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
24002
|
+
tokens.push({
|
|
24003
|
+
type: "string",
|
|
24004
|
+
value: out,
|
|
24005
|
+
pos: start
|
|
24006
|
+
});
|
|
24007
|
+
continue;
|
|
24008
|
+
}
|
|
24009
|
+
if (isIdentStart(ch)) {
|
|
24010
|
+
const start = i;
|
|
24011
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
24012
|
+
const text = source.slice(start, i);
|
|
24013
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
24014
|
+
type: "keyword",
|
|
24015
|
+
keyword: keywordOf(text),
|
|
24016
|
+
pos: start
|
|
24017
|
+
});
|
|
24018
|
+
else tokens.push({
|
|
24019
|
+
type: "identifier",
|
|
24020
|
+
name: text,
|
|
24021
|
+
pos: start
|
|
24022
|
+
});
|
|
24023
|
+
continue;
|
|
24024
|
+
}
|
|
24025
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
24026
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
24027
|
+
tokens.push({
|
|
24028
|
+
type: "punct",
|
|
24029
|
+
punct: two,
|
|
24030
|
+
pos: i
|
|
24031
|
+
});
|
|
24032
|
+
i += 2;
|
|
24033
|
+
continue;
|
|
24034
|
+
}
|
|
24035
|
+
if (isSinglePunct(ch)) {
|
|
24036
|
+
tokens.push({
|
|
24037
|
+
type: "punct",
|
|
24038
|
+
punct: ch,
|
|
24039
|
+
pos: i
|
|
24040
|
+
});
|
|
24041
|
+
i += 1;
|
|
24042
|
+
continue;
|
|
24043
|
+
}
|
|
24044
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
24045
|
+
}
|
|
24046
|
+
tokens.push({
|
|
24047
|
+
type: "eof",
|
|
24048
|
+
pos: n
|
|
24049
|
+
});
|
|
24050
|
+
return tokens;
|
|
24051
|
+
}
|
|
24052
|
+
function keywordOf(text) {
|
|
24053
|
+
if (text === "true") return "true";
|
|
24054
|
+
if (text === "false") return "false";
|
|
24055
|
+
return "null";
|
|
24056
|
+
}
|
|
24057
|
+
function isSinglePunct(ch) {
|
|
24058
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
24059
|
+
}
|
|
24060
|
+
/**
|
|
24061
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
23922
24062
|
*
|
|
23923
|
-
*
|
|
23924
|
-
*
|
|
24063
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
24064
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
24065
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
24066
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
24067
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
24068
|
+
* that references a since-removed builtin degrades at read.
|
|
24069
|
+
*
|
|
24070
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
24071
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
23925
24072
|
*/
|
|
23926
|
-
|
|
23927
|
-
|
|
23928
|
-
"
|
|
23929
|
-
"
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
}
|
|
23942
|
-
|
|
23943
|
-
|
|
23944
|
-
|
|
23945
|
-
|
|
24073
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
24074
|
+
var BINARY_PRECEDENCE = {
|
|
24075
|
+
"||": 1,
|
|
24076
|
+
"&&": 2,
|
|
24077
|
+
"==": 3,
|
|
24078
|
+
"!=": 3,
|
|
24079
|
+
"<": 4,
|
|
24080
|
+
"<=": 4,
|
|
24081
|
+
">": 4,
|
|
24082
|
+
">=": 4,
|
|
24083
|
+
"+": 5,
|
|
24084
|
+
"-": 5,
|
|
24085
|
+
"*": 6,
|
|
24086
|
+
"/": 6,
|
|
24087
|
+
"%": 6
|
|
24088
|
+
};
|
|
24089
|
+
function isLogicalOp(op) {
|
|
24090
|
+
return op === "&&" || op === "||";
|
|
24091
|
+
}
|
|
24092
|
+
function isBinaryOp(op) {
|
|
24093
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
24094
|
+
}
|
|
24095
|
+
var Parser = class {
|
|
24096
|
+
tokens;
|
|
24097
|
+
pos = 0;
|
|
24098
|
+
nodeCount = 0;
|
|
24099
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
24100
|
+
callees = /* @__PURE__ */ new Set();
|
|
24101
|
+
constructor(tokens) {
|
|
24102
|
+
this.tokens = tokens;
|
|
24103
|
+
}
|
|
24104
|
+
parse() {
|
|
24105
|
+
const ast = this.parseTernary();
|
|
24106
|
+
const tok = this.peek();
|
|
24107
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
24108
|
+
return {
|
|
24109
|
+
ast,
|
|
24110
|
+
identifiers: this.identifiers,
|
|
24111
|
+
callees: this.callees,
|
|
24112
|
+
nodeCount: this.nodeCount
|
|
24113
|
+
};
|
|
24114
|
+
}
|
|
24115
|
+
peek() {
|
|
24116
|
+
return this.tokens[this.pos];
|
|
24117
|
+
}
|
|
24118
|
+
next() {
|
|
24119
|
+
return this.tokens[this.pos++];
|
|
24120
|
+
}
|
|
24121
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
24122
|
+
expectPunct(punct) {
|
|
24123
|
+
const tok = this.peek();
|
|
24124
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
24125
|
+
this.pos += 1;
|
|
24126
|
+
}
|
|
24127
|
+
matchPunct(punct) {
|
|
24128
|
+
const tok = this.peek();
|
|
24129
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
24130
|
+
this.pos += 1;
|
|
24131
|
+
return true;
|
|
24132
|
+
}
|
|
24133
|
+
return false;
|
|
24134
|
+
}
|
|
24135
|
+
countNode() {
|
|
24136
|
+
this.nodeCount += 1;
|
|
24137
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
24138
|
+
}
|
|
24139
|
+
parseTernary() {
|
|
24140
|
+
const test = this.parseBinary(1);
|
|
24141
|
+
if (this.matchPunct("?")) {
|
|
24142
|
+
const consequent = this.parseTernary();
|
|
24143
|
+
this.expectPunct(":");
|
|
24144
|
+
const alternate = this.parseTernary();
|
|
24145
|
+
this.countNode();
|
|
24146
|
+
return {
|
|
24147
|
+
kind: "conditional",
|
|
24148
|
+
test,
|
|
24149
|
+
consequent,
|
|
24150
|
+
alternate
|
|
24151
|
+
};
|
|
24152
|
+
}
|
|
24153
|
+
return test;
|
|
24154
|
+
}
|
|
24155
|
+
parseBinary(minPrec) {
|
|
24156
|
+
let left = this.parseUnary();
|
|
24157
|
+
for (;;) {
|
|
24158
|
+
const tok = this.peek();
|
|
24159
|
+
if (tok.type !== "punct") break;
|
|
24160
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
24161
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
24162
|
+
const op = tok.punct;
|
|
24163
|
+
this.pos += 1;
|
|
24164
|
+
const right = this.parseBinary(prec + 1);
|
|
24165
|
+
this.countNode();
|
|
24166
|
+
if (isLogicalOp(op)) left = {
|
|
24167
|
+
kind: "logical",
|
|
24168
|
+
op,
|
|
24169
|
+
left,
|
|
24170
|
+
right
|
|
24171
|
+
};
|
|
24172
|
+
else if (isBinaryOp(op)) left = {
|
|
24173
|
+
kind: "binary",
|
|
24174
|
+
op,
|
|
24175
|
+
left,
|
|
24176
|
+
right
|
|
24177
|
+
};
|
|
24178
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
24179
|
+
}
|
|
24180
|
+
return left;
|
|
24181
|
+
}
|
|
24182
|
+
parseUnary() {
|
|
24183
|
+
const tok = this.peek();
|
|
24184
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
24185
|
+
const op = tok.punct;
|
|
24186
|
+
this.pos += 1;
|
|
24187
|
+
const operand = this.parseUnary();
|
|
24188
|
+
this.countNode();
|
|
24189
|
+
return {
|
|
24190
|
+
kind: "unary",
|
|
24191
|
+
op,
|
|
24192
|
+
operand
|
|
24193
|
+
};
|
|
24194
|
+
}
|
|
24195
|
+
return this.parsePrimary();
|
|
24196
|
+
}
|
|
24197
|
+
parsePrimary() {
|
|
24198
|
+
const tok = this.next();
|
|
24199
|
+
switch (tok.type) {
|
|
24200
|
+
case "number":
|
|
24201
|
+
this.countNode();
|
|
24202
|
+
return {
|
|
24203
|
+
kind: "literal",
|
|
24204
|
+
value: tok.value
|
|
24205
|
+
};
|
|
24206
|
+
case "string":
|
|
24207
|
+
this.countNode();
|
|
24208
|
+
return {
|
|
24209
|
+
kind: "literal",
|
|
24210
|
+
value: tok.value
|
|
24211
|
+
};
|
|
24212
|
+
case "keyword":
|
|
24213
|
+
this.countNode();
|
|
24214
|
+
return {
|
|
24215
|
+
kind: "literal",
|
|
24216
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
24217
|
+
};
|
|
24218
|
+
case "identifier": {
|
|
24219
|
+
const nextTok = this.peek();
|
|
24220
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
24221
|
+
this.identifiers.add(tok.name);
|
|
24222
|
+
this.countNode();
|
|
24223
|
+
return {
|
|
24224
|
+
kind: "identifier",
|
|
24225
|
+
name: tok.name
|
|
24226
|
+
};
|
|
24227
|
+
}
|
|
24228
|
+
case "punct":
|
|
24229
|
+
if (tok.punct === "(") {
|
|
24230
|
+
const inner = this.parseTernary();
|
|
24231
|
+
this.expectPunct(")");
|
|
24232
|
+
return inner;
|
|
24233
|
+
}
|
|
24234
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
24235
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
24236
|
+
}
|
|
24237
|
+
}
|
|
24238
|
+
parseCall(callee, pos) {
|
|
24239
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
24240
|
+
this.expectPunct("(");
|
|
24241
|
+
const args = [];
|
|
24242
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
24243
|
+
args.push(this.parseTernary());
|
|
24244
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
24245
|
+
if (this.matchPunct(",")) continue;
|
|
24246
|
+
this.expectPunct(")");
|
|
24247
|
+
break;
|
|
24248
|
+
}
|
|
24249
|
+
this.callees.add(callee);
|
|
24250
|
+
this.countNode();
|
|
24251
|
+
return {
|
|
24252
|
+
kind: "call",
|
|
24253
|
+
callee,
|
|
24254
|
+
args
|
|
24255
|
+
};
|
|
24256
|
+
}
|
|
24257
|
+
};
|
|
24258
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
24259
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
24260
|
+
function parseExpression(source) {
|
|
24261
|
+
return new Parser(tokenize(source)).parse();
|
|
24262
|
+
}
|
|
23946
24263
|
/**
|
|
23947
|
-
*
|
|
23948
|
-
*
|
|
23949
|
-
*
|
|
23950
|
-
*
|
|
23951
|
-
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
23952
|
-
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
23953
|
-
* spawning, builds a name derived from the parent, and produces a
|
|
23954
|
-
* stableId tied to the parent so boot-restore can reconstruct the
|
|
23955
|
-
* relationship.
|
|
24264
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
24265
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
24266
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
24267
|
+
* one per read on a hot resolve path.
|
|
23956
24268
|
*
|
|
23957
|
-
*
|
|
23958
|
-
*
|
|
23959
|
-
*
|
|
23960
|
-
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
23961
|
-
* one canonical mapping was over-prescriptive and added a layer of
|
|
23962
|
-
* indirection without saving meaningful code at call sites — the
|
|
23963
|
-
* driver knows its own hardware best.
|
|
24269
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
24270
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
24271
|
+
* callers is safe and maximises hit rate.
|
|
23964
24272
|
*/
|
|
24273
|
+
var cache = /* @__PURE__ */ new Map();
|
|
24274
|
+
function getCached(source) {
|
|
24275
|
+
const hit = cache.get(source);
|
|
24276
|
+
if (hit !== void 0) {
|
|
24277
|
+
cache.delete(source);
|
|
24278
|
+
cache.set(source, hit);
|
|
24279
|
+
return hit;
|
|
24280
|
+
}
|
|
24281
|
+
let result;
|
|
24282
|
+
try {
|
|
24283
|
+
result = {
|
|
24284
|
+
ok: true,
|
|
24285
|
+
parsed: parseExpression(source)
|
|
24286
|
+
};
|
|
24287
|
+
} catch (err) {
|
|
24288
|
+
result = {
|
|
24289
|
+
ok: false,
|
|
24290
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
24291
|
+
};
|
|
24292
|
+
}
|
|
24293
|
+
cache.set(source, result);
|
|
24294
|
+
if (cache.size > 256) {
|
|
24295
|
+
const oldest = cache.keys().next().value;
|
|
24296
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
24297
|
+
}
|
|
24298
|
+
return result;
|
|
24299
|
+
}
|
|
24300
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
24301
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
24302
|
+
function compileExpressionSafe(source) {
|
|
24303
|
+
return getCached(source);
|
|
24304
|
+
}
|
|
24305
|
+
Object.freeze({});
|
|
23965
24306
|
/**
|
|
23966
|
-
*
|
|
23967
|
-
*
|
|
23968
|
-
*
|
|
23969
|
-
*
|
|
23970
|
-
*
|
|
23971
|
-
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
23972
|
-
* any role works, including non-accessory ones like Doorbell).
|
|
24307
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
24308
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
24309
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
24310
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
24311
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
23973
24312
|
*/
|
|
23974
|
-
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
|
|
23978
|
-
|
|
23979
|
-
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
|
|
23983
|
-
|
|
23984
|
-
|
|
23985
|
-
|
|
23986
|
-
|
|
23987
|
-
|
|
24313
|
+
function validateExpressionSource(src) {
|
|
24314
|
+
const names = Object.keys(src.bindings);
|
|
24315
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
24316
|
+
for (const name of names) {
|
|
24317
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
24318
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
24319
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
24320
|
+
}
|
|
24321
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
24322
|
+
if (!compiled.ok) return compiled.error;
|
|
24323
|
+
const bound = new Set(names);
|
|
24324
|
+
for (const id of compiled.parsed.identifiers) {
|
|
24325
|
+
if (id === "now") continue;
|
|
24326
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
24327
|
+
}
|
|
24328
|
+
return null;
|
|
24329
|
+
}
|
|
24330
|
+
var ExpressionBindingSourceSchema = union([
|
|
24331
|
+
object({
|
|
24332
|
+
kind: literal("field").optional(),
|
|
24333
|
+
sourceKey: string(),
|
|
24334
|
+
cap: string(),
|
|
24335
|
+
fieldPath: string()
|
|
24336
|
+
}),
|
|
24337
|
+
object({
|
|
24338
|
+
kind: literal("literal"),
|
|
24339
|
+
value: union([
|
|
24340
|
+
string(),
|
|
24341
|
+
number(),
|
|
24342
|
+
boolean(),
|
|
24343
|
+
_null()
|
|
24344
|
+
])
|
|
24345
|
+
}),
|
|
24346
|
+
object({
|
|
24347
|
+
kind: literal("global"),
|
|
24348
|
+
sourceStableId: string(),
|
|
24349
|
+
cap: string(),
|
|
24350
|
+
fieldPath: string()
|
|
24351
|
+
})
|
|
24352
|
+
]);
|
|
24353
|
+
object({
|
|
24354
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
24355
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
24356
|
+
}).superRefine((src, ctx) => {
|
|
24357
|
+
const err = validateExpressionSource(src);
|
|
24358
|
+
if (err !== null) ctx.addIssue({
|
|
24359
|
+
code: "custom",
|
|
24360
|
+
message: err,
|
|
24361
|
+
path: ["expr"]
|
|
24362
|
+
});
|
|
24363
|
+
});
|
|
23988
24364
|
Object.freeze({
|
|
23989
24365
|
"accessories.setChildHidden": {
|
|
23990
24366
|
capName: "accessories",
|
|
@@ -24808,6 +25184,12 @@ Object.freeze({
|
|
|
24808
25184
|
addonId: null,
|
|
24809
25185
|
access: "view"
|
|
24810
25186
|
},
|
|
25187
|
+
"coreBlocks.restart": {
|
|
25188
|
+
capName: "core-blocks",
|
|
25189
|
+
capScope: "system",
|
|
25190
|
+
addonId: null,
|
|
25191
|
+
access: "create"
|
|
25192
|
+
},
|
|
24811
25193
|
"coreBlocks.setEnabled": {
|
|
24812
25194
|
capName: "core-blocks",
|
|
24813
25195
|
capScope: "system",
|
|
@@ -25444,12 +25826,6 @@ Object.freeze({
|
|
|
25444
25826
|
addonId: null,
|
|
25445
25827
|
access: "create"
|
|
25446
25828
|
},
|
|
25447
|
-
"deviceManager.setDeviceLinks": {
|
|
25448
|
-
capName: "device-manager",
|
|
25449
|
-
capScope: "system",
|
|
25450
|
-
addonId: null,
|
|
25451
|
-
access: "create"
|
|
25452
|
-
},
|
|
25453
25829
|
"deviceManager.setDisabled": {
|
|
25454
25830
|
capName: "device-manager",
|
|
25455
25831
|
capScope: "system",
|
|
@@ -26962,6 +27338,12 @@ Object.freeze({
|
|
|
26962
27338
|
addonId: null,
|
|
26963
27339
|
access: "delete"
|
|
26964
27340
|
},
|
|
27341
|
+
"pipelineAnalytics.completeRetrainTrack": {
|
|
27342
|
+
capName: "pipeline-analytics",
|
|
27343
|
+
capScope: "device",
|
|
27344
|
+
addonId: null,
|
|
27345
|
+
access: "create"
|
|
27346
|
+
},
|
|
26965
27347
|
"pipelineAnalytics.deleteDeviceEvents": {
|
|
26966
27348
|
capName: "pipeline-analytics",
|
|
26967
27349
|
capScope: "device",
|
|
@@ -26974,6 +27356,12 @@ Object.freeze({
|
|
|
26974
27356
|
addonId: null,
|
|
26975
27357
|
access: "delete"
|
|
26976
27358
|
},
|
|
27359
|
+
"pipelineAnalytics.deselectRetrainFrame": {
|
|
27360
|
+
capName: "pipeline-analytics",
|
|
27361
|
+
capScope: "device",
|
|
27362
|
+
addonId: null,
|
|
27363
|
+
access: "create"
|
|
27364
|
+
},
|
|
26977
27365
|
"pipelineAnalytics.getActiveTracks": {
|
|
26978
27366
|
capName: "pipeline-analytics",
|
|
26979
27367
|
capScope: "device",
|
|
@@ -27034,6 +27422,18 @@ Object.freeze({
|
|
|
27034
27422
|
addonId: null,
|
|
27035
27423
|
access: "view"
|
|
27036
27424
|
},
|
|
27425
|
+
"pipelineAnalytics.getRetrainExportUrl": {
|
|
27426
|
+
capName: "pipeline-analytics",
|
|
27427
|
+
capScope: "device",
|
|
27428
|
+
addonId: null,
|
|
27429
|
+
access: "view"
|
|
27430
|
+
},
|
|
27431
|
+
"pipelineAnalytics.getRetrainFrameImage": {
|
|
27432
|
+
capName: "pipeline-analytics",
|
|
27433
|
+
capScope: "device",
|
|
27434
|
+
addonId: null,
|
|
27435
|
+
access: "view"
|
|
27436
|
+
},
|
|
27037
27437
|
"pipelineAnalytics.getSensorEvents": {
|
|
27038
27438
|
capName: "pipeline-analytics",
|
|
27039
27439
|
capScope: "device",
|
|
@@ -27088,6 +27488,24 @@ Object.freeze({
|
|
|
27088
27488
|
addonId: null,
|
|
27089
27489
|
access: "view"
|
|
27090
27490
|
},
|
|
27491
|
+
"pipelineAnalytics.listRetrainAnnotations": {
|
|
27492
|
+
capName: "pipeline-analytics",
|
|
27493
|
+
capScope: "device",
|
|
27494
|
+
addonId: null,
|
|
27495
|
+
access: "view"
|
|
27496
|
+
},
|
|
27497
|
+
"pipelineAnalytics.listRetrainFrames": {
|
|
27498
|
+
capName: "pipeline-analytics",
|
|
27499
|
+
capScope: "device",
|
|
27500
|
+
addonId: null,
|
|
27501
|
+
access: "view"
|
|
27502
|
+
},
|
|
27503
|
+
"pipelineAnalytics.listRetrainStaging": {
|
|
27504
|
+
capName: "pipeline-analytics",
|
|
27505
|
+
capScope: "device",
|
|
27506
|
+
addonId: null,
|
|
27507
|
+
access: "view"
|
|
27508
|
+
},
|
|
27091
27509
|
"pipelineAnalytics.listTrackMedia": {
|
|
27092
27510
|
capName: "pipeline-analytics",
|
|
27093
27511
|
capScope: "device",
|
|
@@ -27100,6 +27518,12 @@ Object.freeze({
|
|
|
27100
27518
|
addonId: null,
|
|
27101
27519
|
access: "view"
|
|
27102
27520
|
},
|
|
27521
|
+
"pipelineAnalytics.proposeRetrainAnnotations": {
|
|
27522
|
+
capName: "pipeline-analytics",
|
|
27523
|
+
capScope: "device",
|
|
27524
|
+
addonId: null,
|
|
27525
|
+
access: "create"
|
|
27526
|
+
},
|
|
27103
27527
|
"pipelineAnalytics.pruneEvents": {
|
|
27104
27528
|
capName: "pipeline-analytics",
|
|
27105
27529
|
capScope: "device",
|
|
@@ -27130,12 +27554,30 @@ Object.freeze({
|
|
|
27130
27554
|
addonId: null,
|
|
27131
27555
|
access: "create"
|
|
27132
27556
|
},
|
|
27557
|
+
"pipelineAnalytics.restageRetrainTrack": {
|
|
27558
|
+
capName: "pipeline-analytics",
|
|
27559
|
+
capScope: "device",
|
|
27560
|
+
addonId: null,
|
|
27561
|
+
access: "create"
|
|
27562
|
+
},
|
|
27563
|
+
"pipelineAnalytics.saveRetrainAnnotations": {
|
|
27564
|
+
capName: "pipeline-analytics",
|
|
27565
|
+
capScope: "device",
|
|
27566
|
+
addonId: null,
|
|
27567
|
+
access: "create"
|
|
27568
|
+
},
|
|
27133
27569
|
"pipelineAnalytics.searchObjectEvents": {
|
|
27134
27570
|
capName: "pipeline-analytics",
|
|
27135
27571
|
capScope: "device",
|
|
27136
27572
|
addonId: null,
|
|
27137
27573
|
access: "view"
|
|
27138
27574
|
},
|
|
27575
|
+
"pipelineAnalytics.selectRetrainFrames": {
|
|
27576
|
+
capName: "pipeline-analytics",
|
|
27577
|
+
capScope: "device",
|
|
27578
|
+
addonId: null,
|
|
27579
|
+
access: "create"
|
|
27580
|
+
},
|
|
27139
27581
|
"pipelineAnalytics.setTrackFlags": {
|
|
27140
27582
|
capName: "pipeline-analytics",
|
|
27141
27583
|
capScope: "device",
|
|
@@ -28528,6 +28970,12 @@ Object.freeze({
|
|
|
28528
28970
|
addonId: null,
|
|
28529
28971
|
access: "create"
|
|
28530
28972
|
},
|
|
28973
|
+
"streamBroker.fetchEventMedia": {
|
|
28974
|
+
capName: "stream-broker",
|
|
28975
|
+
capScope: "system",
|
|
28976
|
+
addonId: null,
|
|
28977
|
+
access: "create"
|
|
28978
|
+
},
|
|
28531
28979
|
"streamBroker.getAllRtspEntries": {
|
|
28532
28980
|
capName: "stream-broker",
|
|
28533
28981
|
capScope: "system",
|
|
@@ -28612,6 +29060,12 @@ Object.freeze({
|
|
|
28612
29060
|
addonId: null,
|
|
28613
29061
|
access: "create"
|
|
28614
29062
|
},
|
|
29063
|
+
"streamBroker.produceEventMedia": {
|
|
29064
|
+
capName: "stream-broker",
|
|
29065
|
+
capScope: "system",
|
|
29066
|
+
addonId: null,
|
|
29067
|
+
access: "create"
|
|
29068
|
+
},
|
|
28615
29069
|
"streamBroker.publishCameraStream": {
|
|
28616
29070
|
capName: "stream-broker",
|
|
28617
29071
|
capScope: "system",
|
|
@@ -43581,37 +44035,37 @@ function errMsg$8(err) {
|
|
|
43581
44035
|
}
|
|
43582
44036
|
//#endregion
|
|
43583
44037
|
//#region src/mappers/builders/doorbell-delivery.ts
|
|
43584
|
-
function isRecord(value) {
|
|
44038
|
+
function isRecord$1(value) {
|
|
43585
44039
|
return typeof value === "object" && value !== null;
|
|
43586
44040
|
}
|
|
43587
44041
|
function numberOrNull(value) {
|
|
43588
44042
|
return typeof value === "number" ? value : null;
|
|
43589
44043
|
}
|
|
43590
44044
|
function isConnectionLike(value) {
|
|
43591
|
-
return isRecord(value) && typeof value["hasEventNotifications"] === "function";
|
|
44045
|
+
return isRecord$1(value) && typeof value["hasEventNotifications"] === "function";
|
|
43592
44046
|
}
|
|
43593
|
-
function isIterable(value) {
|
|
43594
|
-
return isRecord(value) && typeof value[Symbol.iterator] === "function";
|
|
44047
|
+
function isIterable$1(value) {
|
|
44048
|
+
return isRecord$1(value) && typeof value[Symbol.iterator] === "function";
|
|
43595
44049
|
}
|
|
43596
44050
|
/** `accessory._server.httpServer.connections`, or null at any missing hop. */
|
|
43597
|
-
function readConnections(accessory) {
|
|
43598
|
-
if (!isRecord(accessory)) return null;
|
|
44051
|
+
function readConnections$1(accessory) {
|
|
44052
|
+
if (!isRecord$1(accessory)) return null;
|
|
43599
44053
|
const server = accessory["_server"];
|
|
43600
|
-
if (!isRecord(server)) return null;
|
|
44054
|
+
if (!isRecord$1(server)) return null;
|
|
43601
44055
|
const httpServer = server["httpServer"];
|
|
43602
|
-
if (!isRecord(httpServer)) return null;
|
|
44056
|
+
if (!isRecord$1(httpServer)) return null;
|
|
43603
44057
|
const connections = httpServer["connections"];
|
|
43604
|
-
return isIterable(connections) ? connections : null;
|
|
44058
|
+
return isIterable$1(connections) ? connections : null;
|
|
43605
44059
|
}
|
|
43606
44060
|
/**
|
|
43607
44061
|
* Probe how far a ring on `characteristic` of `accessory` can travel RIGHT NOW.
|
|
43608
44062
|
* Pure with respect to HAP state — it only reads. Never throws.
|
|
43609
44063
|
*/
|
|
43610
44064
|
function describeDoorbellDelivery(accessory, characteristic) {
|
|
43611
|
-
const aid = isRecord(accessory) ? numberOrNull(accessory["aid"]) : null;
|
|
43612
|
-
const iid = isRecord(characteristic) ? numberOrNull(characteristic["iid"]) : null;
|
|
43613
|
-
const serverPublished = isRecord(accessory) && isRecord(accessory["_server"]);
|
|
43614
|
-
const connections = readConnections(accessory);
|
|
44065
|
+
const aid = isRecord$1(accessory) ? numberOrNull(accessory["aid"]) : null;
|
|
44066
|
+
const iid = isRecord$1(characteristic) ? numberOrNull(characteristic["iid"]) : null;
|
|
44067
|
+
const serverPublished = isRecord$1(accessory) && isRecord$1(accessory["_server"]);
|
|
44068
|
+
const connections = readConnections$1(accessory);
|
|
43615
44069
|
if (connections === null) return {
|
|
43616
44070
|
aid,
|
|
43617
44071
|
iid,
|
|
@@ -43723,10 +44177,16 @@ async function buildMotionSensor(bctx, existing = null) {
|
|
|
43723
44177
|
resetTimer = null;
|
|
43724
44178
|
}, RESET_DEBOUNCE_MS);
|
|
43725
44179
|
};
|
|
44180
|
+
const motionLog = ctx.logger.withTags({ deviceId: numericDeviceId });
|
|
44181
|
+
const hksvTrigger = existing !== null;
|
|
43726
44182
|
const unsubscribe = ctx.eventBus.subscribe({ category: EventCategory.MotionOnMotionChanged }, (event) => {
|
|
43727
44183
|
if (event.data.deviceId !== numericDeviceId) return;
|
|
43728
44184
|
const detected = event.data.detected === true;
|
|
43729
44185
|
motionService.updateCharacteristic(Characteristic.MotionDetected, detected);
|
|
44186
|
+
motionLog.debug("export-hap: motion pushed to HomeKit", { meta: {
|
|
44187
|
+
detected,
|
|
44188
|
+
hksvTrigger
|
|
44189
|
+
} });
|
|
43730
44190
|
if (detected) armReset();
|
|
43731
44191
|
else if (resetTimer) {
|
|
43732
44192
|
clearTimeout(resetTimer);
|
|
@@ -44103,36 +44563,84 @@ async function probe(call, label, log) {
|
|
|
44103
44563
|
}
|
|
44104
44564
|
}
|
|
44105
44565
|
//#endregion
|
|
44106
|
-
//#region src/hksv/
|
|
44566
|
+
//#region src/hksv/controller-census.ts
|
|
44567
|
+
var EMPTY_CENSUS = {
|
|
44568
|
+
serverPublished: false,
|
|
44569
|
+
connections: 0,
|
|
44570
|
+
adminConnections: 0,
|
|
44571
|
+
nonAdminConnections: 0,
|
|
44572
|
+
unverifiedConnections: 0,
|
|
44573
|
+
pairedControllers: 0,
|
|
44574
|
+
pairedAdmins: 0
|
|
44575
|
+
};
|
|
44576
|
+
function isRecord(value) {
|
|
44577
|
+
return typeof value === "object" && value !== null;
|
|
44578
|
+
}
|
|
44579
|
+
function isIterable(value) {
|
|
44580
|
+
return isRecord(value) && typeof value[Symbol.iterator] === "function";
|
|
44581
|
+
}
|
|
44582
|
+
function isAccessoryInfoLike(value) {
|
|
44583
|
+
return isRecord(value) && isRecord(value["pairedClients"]) && typeof value["hasAdminPermissions"] === "function";
|
|
44584
|
+
}
|
|
44585
|
+
/** `accessory._server.httpServer.connections`, or null at any missing hop. */
|
|
44586
|
+
function readConnections(accessory) {
|
|
44587
|
+
const server = accessory["_server"];
|
|
44588
|
+
if (!isRecord(server)) return null;
|
|
44589
|
+
const httpServer = server["httpServer"];
|
|
44590
|
+
if (!isRecord(httpServer)) return null;
|
|
44591
|
+
const connections = httpServer["connections"];
|
|
44592
|
+
return isIterable(connections) ? connections : null;
|
|
44593
|
+
}
|
|
44107
44594
|
/**
|
|
44108
|
-
*
|
|
44109
|
-
*
|
|
44110
|
-
*
|
|
44111
|
-
* ## The rule this file exists to enforce
|
|
44112
|
-
*
|
|
44113
|
-
* Never advertise something we cannot serve. That is not a slogan here: it is
|
|
44114
|
-
* the diagnosis of [D50](../../../../docs/decisions/adr-0050.md) — an
|
|
44115
|
-
* advertised `recording` whose delegate yielded nothing put every motion-capable
|
|
44116
|
-
* camera into a ~12 s timeout loop every 20-60 s, all day. So every number below
|
|
44117
|
-
* is derived from the picked source (`recording-source.ts`) or from a measured
|
|
44118
|
-
* property of the sink, and none of them is a plausible-looking constant.
|
|
44119
|
-
*
|
|
44120
|
-
* ## The fragment length is the subtle one
|
|
44121
|
-
*
|
|
44122
|
-
* HKSV requires every media fragment to be **no longer** than the length the
|
|
44123
|
-
* controller selected. On the copy branch the fragment length is the SOURCE's
|
|
44124
|
-
* key-frame cadence ([D80](../../../../docs/decisions/adr-0080.md)) — we do not
|
|
44125
|
-
* get to choose it, we can only be honest about it. So:
|
|
44126
|
-
*
|
|
44127
|
-
* - when the camera reports its GOP (`stream-params`), the advertised length is
|
|
44128
|
-
* the smallest offered value that COVERS it;
|
|
44129
|
-
* - when it does not, we advertise the 4000 ms every HKSV camera uses and the
|
|
44130
|
-
* delegate warns at `warn` with `tags: { deviceId }` if the fragments that
|
|
44131
|
-
* actually arrive are longer.
|
|
44132
|
-
*
|
|
44133
|
-
* A camera whose GOP exceeds the longest value we offer does not advertise
|
|
44134
|
-
* recording at all. See {@link deriveFragmentLengthMs}.
|
|
44595
|
+
* Census the accessory's HAP connections. Pure with respect to HAP state — it
|
|
44596
|
+
* only reads — and never throws.
|
|
44135
44597
|
*/
|
|
44598
|
+
function describeHapControllers(accessory) {
|
|
44599
|
+
if (!isRecord(accessory)) return EMPTY_CENSUS;
|
|
44600
|
+
const info = accessory["_accessoryInfo"];
|
|
44601
|
+
const paired = isAccessoryInfoLike(info) ? Object.keys(info.pairedClients) : [];
|
|
44602
|
+
const pairedAdmins = isAccessoryInfoLike(info) ? paired.filter((username) => info.hasAdminPermissions(username)).length : 0;
|
|
44603
|
+
const connections = readConnections(accessory);
|
|
44604
|
+
if (connections === null) return {
|
|
44605
|
+
...EMPTY_CENSUS,
|
|
44606
|
+
pairedControllers: paired.length,
|
|
44607
|
+
pairedAdmins
|
|
44608
|
+
};
|
|
44609
|
+
let open = 0;
|
|
44610
|
+
let admins = 0;
|
|
44611
|
+
let nonAdmins = 0;
|
|
44612
|
+
let unverified = 0;
|
|
44613
|
+
for (const connection of connections) {
|
|
44614
|
+
open += 1;
|
|
44615
|
+
const username = isRecord(connection) ? connection["username"] : void 0;
|
|
44616
|
+
if (typeof username !== "string" || !isAccessoryInfoLike(info)) {
|
|
44617
|
+
unverified += 1;
|
|
44618
|
+
continue;
|
|
44619
|
+
}
|
|
44620
|
+
if (info.hasAdminPermissions(username)) admins += 1;
|
|
44621
|
+
else nonAdmins += 1;
|
|
44622
|
+
}
|
|
44623
|
+
return {
|
|
44624
|
+
serverPublished: true,
|
|
44625
|
+
connections: open,
|
|
44626
|
+
adminConnections: admins,
|
|
44627
|
+
nonAdminConnections: nonAdmins,
|
|
44628
|
+
unverifiedConnections: unverified,
|
|
44629
|
+
pairedControllers: paired.length,
|
|
44630
|
+
pairedAdmins
|
|
44631
|
+
};
|
|
44632
|
+
}
|
|
44633
|
+
/**
|
|
44634
|
+
* True when NO connected controller may write HKSV state. Every
|
|
44635
|
+
* `SelectedCameraRecordingConfiguration` write such a controller sends is
|
|
44636
|
+
* refused before it reaches us, so the recording configuration can never
|
|
44637
|
+
* arrive and the caller must say so out loud.
|
|
44638
|
+
*/
|
|
44639
|
+
function noAdminControllerConnected(census) {
|
|
44640
|
+
return census.serverPublished && census.connections > 0 && census.adminConnections === 0;
|
|
44641
|
+
}
|
|
44642
|
+
//#endregion
|
|
44643
|
+
//#region src/hksv/recording-options.ts
|
|
44136
44644
|
/**
|
|
44137
44645
|
* The prebuffer we promise. HAP's floor is 4000 ms and its documented sensible
|
|
44138
44646
|
* range is [4000, 8000]; the plane's ring is sized from this, so the two cannot
|
|
@@ -44155,6 +44663,16 @@ var HKSV_FRAGMENT_LENGTHS_MS = [4e3, 8e3];
|
|
|
44155
44663
|
*/
|
|
44156
44664
|
var HKSV_AUDIO_SAMPLE_RATE_HZ = 24e3;
|
|
44157
44665
|
/**
|
|
44666
|
+
* The frame rates the advertised {@link Resolution} may carry, and the only
|
|
44667
|
+
* ones. See {@link normaliseAdvertisedFps} for why the measured rate does not
|
|
44668
|
+
* go in raw.
|
|
44669
|
+
*/
|
|
44670
|
+
var HKSV_ADVERTISED_FRAME_RATES = [
|
|
44671
|
+
15,
|
|
44672
|
+
24,
|
|
44673
|
+
30
|
|
44674
|
+
];
|
|
44675
|
+
/**
|
|
44158
44676
|
* The advertised fragment length for a camera whose key-frame cadence is
|
|
44159
44677
|
* `sourceGopMs`, or `null` when no offered length covers it.
|
|
44160
44678
|
*
|
|
@@ -44170,6 +44688,46 @@ function deriveFragmentLengthMs(sourceGopMs) {
|
|
|
44170
44688
|
return HKSV_FRAGMENT_LENGTHS_MS.find((ms) => ms >= sourceGopMs) ?? null;
|
|
44171
44689
|
}
|
|
44172
44690
|
/**
|
|
44691
|
+
* The frame rate to ADVERTISE for a slot that was measured at `measuredFps` —
|
|
44692
|
+
* the nearest member of {@link HKSV_ADVERTISED_FRAME_RATES}, ties going to the
|
|
44693
|
+
* lower rate.
|
|
44694
|
+
*
|
|
44695
|
+
* The measured rate does not go into the advertisement raw, for two reasons,
|
|
44696
|
+
* and the second one is the serious one.
|
|
44697
|
+
*
|
|
44698
|
+
* **It is the list the controller chooses from.** `[1280, 720, 10]` — 615's
|
|
44699
|
+
* measured 720p slot — is a frame rate no shipping HKSV camera offers, and the
|
|
44700
|
+
* controller has to find an acceptable configuration in what we advertise
|
|
44701
|
+
* before it will write one back.
|
|
44702
|
+
*
|
|
44703
|
+
* **A measurement makes the advertisement UNSTABLE, and hap-nodejs punishes
|
|
44704
|
+
* that by discarding the controller's selection.** `RecordingManagement`
|
|
44705
|
+
* hashes the supported-configuration TLVs and, on restore, keeps the persisted
|
|
44706
|
+
* `selectedConfiguration` only while the hash still matches — otherwise
|
|
44707
|
+
* `deserialize: discarding saved selectedConfiguration`, after which the
|
|
44708
|
+
* accessory answers every HDS `DATA_SEND OPEN` with `INVALID_CONFIGURATION`
|
|
44709
|
+
* and records nothing until the controller happens to re-select. The
|
|
44710
|
+
* advertised resolution is the one hashed input that came from a probe:
|
|
44711
|
+
* camera 590 measured 9 fps on one restart and 10 on the next, on
|
|
44712
|
+
* 2026-08-07/08, so this was a self-inflicted outage waiting on a reboot.
|
|
44713
|
+
* Quantising gives the probe a wide band to move inside without the
|
|
44714
|
+
* advertisement changing at all.
|
|
44715
|
+
*/
|
|
44716
|
+
function normaliseAdvertisedFps(measuredFps) {
|
|
44717
|
+
const fallback = HKSV_ADVERTISED_FRAME_RATES[0] ?? 15;
|
|
44718
|
+
if (!Number.isFinite(measuredFps) || measuredFps <= 0) return fallback;
|
|
44719
|
+
let best = fallback;
|
|
44720
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
44721
|
+
for (const candidate of HKSV_ADVERTISED_FRAME_RATES) {
|
|
44722
|
+
const distance = Math.abs(candidate - measuredFps);
|
|
44723
|
+
if (distance < bestDistance) {
|
|
44724
|
+
best = candidate;
|
|
44725
|
+
bestDistance = distance;
|
|
44726
|
+
}
|
|
44727
|
+
}
|
|
44728
|
+
return best;
|
|
44729
|
+
}
|
|
44730
|
+
/**
|
|
44173
44731
|
* Build the advertisement.
|
|
44174
44732
|
*
|
|
44175
44733
|
* ONE resolution is advertised — the one slot the recording child pulls. HAP's
|
|
@@ -44182,7 +44740,7 @@ function buildRecordingOptions(input) {
|
|
|
44182
44740
|
const resolution = [
|
|
44183
44741
|
input.width,
|
|
44184
44742
|
input.height,
|
|
44185
|
-
|
|
44743
|
+
normaliseAdvertisedFps(input.fps)
|
|
44186
44744
|
];
|
|
44187
44745
|
return {
|
|
44188
44746
|
prebufferLength: HKSV_PREBUFFER_MS,
|
|
@@ -44505,13 +45063,19 @@ var HksvRecordingDelegate = class {
|
|
|
44505
45063
|
updateRecordingActive(active) {
|
|
44506
45064
|
if (active === this.active) return;
|
|
44507
45065
|
this.active = active;
|
|
45066
|
+
const census = this.input.describeControllers();
|
|
44508
45067
|
this.log.info("hksv: recording active changed", {
|
|
44509
45068
|
tags: { deviceId: this.input.deviceId },
|
|
44510
45069
|
meta: {
|
|
44511
45070
|
active,
|
|
44512
|
-
hasConfiguration: this.configuration !== void 0
|
|
45071
|
+
hasConfiguration: this.configuration !== void 0,
|
|
45072
|
+
...census
|
|
44513
45073
|
}
|
|
44514
45074
|
});
|
|
45075
|
+
if (active && this.configuration === void 0 && noAdminControllerConnected(census)) this.log.warn("hksv: recording is ON but NO connected controller holds admin — the recording configuration can never arrive", {
|
|
45076
|
+
tags: { deviceId: this.input.deviceId },
|
|
45077
|
+
meta: { ...census }
|
|
45078
|
+
});
|
|
44515
45079
|
this.reconcile("recording-active");
|
|
44516
45080
|
}
|
|
44517
45081
|
updateRecordingConfiguration(configuration) {
|
|
@@ -44566,13 +45130,14 @@ var HksvRecordingDelegate = class {
|
|
|
44566
45130
|
streamId,
|
|
44567
45131
|
subscription
|
|
44568
45132
|
};
|
|
44569
|
-
const startedAt =
|
|
45133
|
+
const startedAt = this.input.now();
|
|
44570
45134
|
const prebufferSpanMs = source.prebufferSpanMs();
|
|
44571
45135
|
let packets = 0;
|
|
44572
45136
|
let bytes = 0;
|
|
44573
45137
|
let markedLast = false;
|
|
44574
45138
|
let longestFragmentGapMs = 0;
|
|
44575
|
-
let
|
|
45139
|
+
let firstFragmentAt = null;
|
|
45140
|
+
let lastFragmentAt = null;
|
|
44576
45141
|
try {
|
|
44577
45142
|
for await (const packet of subscription.packets()) {
|
|
44578
45143
|
if (signal?.aborted === true) {
|
|
@@ -44588,9 +45153,10 @@ var HksvRecordingDelegate = class {
|
|
|
44588
45153
|
packets += 1;
|
|
44589
45154
|
bytes += packet.data.length;
|
|
44590
45155
|
if (packet.kind === "fragment") {
|
|
44591
|
-
const now =
|
|
44592
|
-
|
|
44593
|
-
|
|
45156
|
+
const now = this.input.now();
|
|
45157
|
+
if (lastFragmentAt === null) firstFragmentAt = now;
|
|
45158
|
+
else longestFragmentGapMs = Math.max(longestFragmentGapMs, now - lastFragmentAt);
|
|
45159
|
+
lastFragmentAt = now;
|
|
44594
45160
|
}
|
|
44595
45161
|
markedLast = markedLast || packet.isLast;
|
|
44596
45162
|
yield {
|
|
@@ -44625,9 +45191,10 @@ var HksvRecordingDelegate = class {
|
|
|
44625
45191
|
streamId,
|
|
44626
45192
|
packets,
|
|
44627
45193
|
bytes,
|
|
44628
|
-
durationMs:
|
|
45194
|
+
durationMs: this.input.now() - startedAt,
|
|
44629
45195
|
prebufferSpanMs,
|
|
44630
45196
|
longestFragmentGapMs,
|
|
45197
|
+
msToFirstFragmentMs: firstFragmentAt === null ? null : firstFragmentAt - startedAt,
|
|
44631
45198
|
closedReason: subscription.closedReason,
|
|
44632
45199
|
markedLast
|
|
44633
45200
|
}
|
|
@@ -44844,6 +45411,8 @@ async function buildHksvRecording(input) {
|
|
|
44844
45411
|
deviceId: numericDeviceId,
|
|
44845
45412
|
isAudioActive: input.isAudioActive,
|
|
44846
45413
|
advertisedFragmentMs: fragmentLengthMs,
|
|
45414
|
+
now: () => Date.now(),
|
|
45415
|
+
describeControllers: () => describeHapControllers(bctx.accessory),
|
|
44847
45416
|
createSource: ({ fragmentMs, audioActive }) => new HksvFragmentSource({
|
|
44848
45417
|
logger: log,
|
|
44849
45418
|
deviceId: numericDeviceId,
|
|
@@ -44854,11 +45423,13 @@ async function buildHksvRecording(input) {
|
|
|
44854
45423
|
audioActive
|
|
44855
45424
|
})
|
|
44856
45425
|
});
|
|
45426
|
+
const advertisedResolution = options.video.resolutions[0];
|
|
44857
45427
|
log.info("export-hap: HKSV ADVERTISED — recording is offered for this camera", { meta: {
|
|
44858
45428
|
brokerId: source.brokerId,
|
|
44859
45429
|
profile: source.profile,
|
|
44860
45430
|
resolution: `${source.width}x${source.height}`,
|
|
44861
|
-
fps,
|
|
45431
|
+
measuredFps: fps,
|
|
45432
|
+
advertisedFps: advertisedResolution?.[2] ?? null,
|
|
44862
45433
|
fragmentLengthMs,
|
|
44863
45434
|
sourceGopMs: gopMs ?? "unknown"
|
|
44864
45435
|
} });
|
|
@@ -45395,8 +45966,21 @@ function syncStateToJson(map) {
|
|
|
45395
45966
|
*/
|
|
45396
45967
|
var DEFAULT_DEVICE_SETTINGS = {
|
|
45397
45968
|
streamPreference: "auto",
|
|
45398
|
-
hksvRecording:
|
|
45969
|
+
hksvRecording: true
|
|
45399
45970
|
};
|
|
45971
|
+
/**
|
|
45972
|
+
* ON unless explicitly switched off — operator decision 2026-08-08 (flipped
|
|
45973
|
+
* from the launch default of off). ABSENT must resolve to ON or the flip is a
|
|
45974
|
+
* lie for every entry persisted before the field existed, so every read goes
|
|
45975
|
+
* through this one resolver (`!== false`), never a scattered `=== true`. The
|
|
45976
|
+
* cost that made off-by-default look prudent is measured and small on the only
|
|
45977
|
+
* branch the recorder accepts (copy: 0.7 % of a core / ~30 MB RSS, D84), and a
|
|
45978
|
+
* camera the recorder cannot copy refuses recording with a logged reason
|
|
45979
|
+
* rather than paying for a transcode.
|
|
45980
|
+
*/
|
|
45981
|
+
function resolveHksvRecording(settings) {
|
|
45982
|
+
return settings?.hksvRecording !== false;
|
|
45983
|
+
}
|
|
45400
45984
|
var HAP_STREAM_PREFERENCE_OPTIONS = [
|
|
45401
45985
|
{
|
|
45402
45986
|
value: "auto",
|
|
@@ -45673,7 +46257,7 @@ var ExportHapAddon = class extends BaseAddon {
|
|
|
45673
46257
|
decodeMemos: this.decodeMemos,
|
|
45674
46258
|
hapDeviceSettings: {
|
|
45675
46259
|
streamPreference: entrySettings.streamPreference ?? "auto",
|
|
45676
|
-
hksvRecording: entrySettings
|
|
46260
|
+
hksvRecording: resolveHksvRecording(entrySettings)
|
|
45677
46261
|
}
|
|
45678
46262
|
}
|
|
45679
46263
|
});
|
|
@@ -46024,7 +46608,7 @@ var ExportHapAddon = class extends BaseAddon {
|
|
|
46024
46608
|
label: "HomeKit recording (Secure Video)",
|
|
46025
46609
|
description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
|
|
46026
46610
|
style: "switch",
|
|
46027
|
-
value: settings
|
|
46611
|
+
value: resolveHksvRecording(settings),
|
|
46028
46612
|
showWhen: {
|
|
46029
46613
|
field: enabledKey,
|
|
46030
46614
|
equals: true
|
|
@@ -46058,7 +46642,7 @@ var ExportHapAddon = class extends BaseAddon {
|
|
|
46058
46642
|
const enabledValue = enabledKey in patch ? Boolean(patch[enabledKey]) : wasEnabled;
|
|
46059
46643
|
const streamPreferenceRaw = streamPreferenceKey in patch ? patch[streamPreferenceKey] : current?.settings?.streamPreference;
|
|
46060
46644
|
const streamPreference = typeof streamPreferenceRaw === "string" && streamPreferenceRaw.trim().length > 0 ? streamPreferenceRaw : "auto";
|
|
46061
|
-
const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : current?.settings
|
|
46645
|
+
const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : resolveHksvRecording(current?.settings);
|
|
46062
46646
|
const nextSettings = {
|
|
46063
46647
|
...current?.settings ?? DEFAULT_DEVICE_SETTINGS,
|
|
46064
46648
|
streamPreference,
|
|
@@ -46074,7 +46658,7 @@ var ExportHapAddon = class extends BaseAddon {
|
|
|
46074
46658
|
return { success: true };
|
|
46075
46659
|
}
|
|
46076
46660
|
const currentPref = current?.settings?.streamPreference ?? "auto";
|
|
46077
|
-
const currentHksv = current?.settings
|
|
46661
|
+
const currentHksv = resolveHksvRecording(current?.settings);
|
|
46078
46662
|
await this.updateEntrySettings(deviceIdStr, nextSettings);
|
|
46079
46663
|
if (currentPref !== streamPreference || currentHksv !== hksvRecording) {
|
|
46080
46664
|
log.info("export-hap: per-camera export settings changed — refreshing accessory", { meta: {
|
|
@@ -46129,4 +46713,4 @@ function errMsg(err) {
|
|
|
46129
46713
|
return err instanceof Error ? err.message : String(err);
|
|
46130
46714
|
}
|
|
46131
46715
|
//#endregion
|
|
46132
|
-
export { ExportHapAddon, ExportHapAddon as default, unpublishAccessory as i, initHapStorage as n, publishStandalone as r, deriveUsername as t };
|
|
46716
|
+
export { ExportHapAddon, ExportHapAddon as default, unpublishAccessory as i, initHapStorage as n, publishStandalone as r, resolveHksvRecording, deriveUsername as t };
|