@camstack/types 1.2.46 → 1.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/addon.js +4 -3
  2. package/dist/addon.mjs +4 -3
  3. package/dist/{fmp4-box-splitter-B53u9-Nu.mjs → canonical-hash-rO1sRmEK.mjs} +34 -34
  4. package/dist/capabilities/core-blocks.cap.d.ts +52 -0
  5. package/dist/capabilities/device-manager.cap.d.ts +11 -427
  6. package/dist/capabilities/face-gallery.cap.d.ts +4 -0
  7. package/dist/capabilities/index.d.ts +2 -2
  8. package/dist/capabilities/motion-detection.cap.d.ts +6 -1
  9. package/dist/capabilities/notification-rules.cap.d.ts +24 -24
  10. package/dist/capabilities/oauth-integration.cap.d.ts +4 -0
  11. package/dist/capabilities/osd-manager.cap.d.ts +12 -12
  12. package/dist/capabilities/pipeline-analytics.cap.d.ts +7 -1
  13. package/dist/capabilities/sso-bridge.cap.d.ts +3 -0
  14. package/dist/capabilities/stream-broker.cap.d.ts +1 -0
  15. package/dist/capabilities/user-management.cap.d.ts +3 -1
  16. package/dist/capabilities/videoclips.cap.d.ts +5 -0
  17. package/dist/device/declared-device.d.ts +197 -0
  18. package/dist/device/device-binding.d.ts +15 -9
  19. package/dist/device/device-management.d.ts +1 -83
  20. package/dist/device/index.d.ts +20 -19
  21. package/dist/expression/binding-source.d.ts +85 -0
  22. package/dist/expression/{link-expression.d.ts → expression-source.d.ts} +22 -13
  23. package/dist/expression/index.d.ts +17 -14
  24. package/dist/expression/limits.d.ts +1 -1
  25. package/dist/generated/addon-api.d.ts +11 -11
  26. package/dist/generated/device-proxy.d.ts +1 -1
  27. package/dist/generated/system-proxy.d.ts +1 -1
  28. package/dist/index.d.ts +9 -8
  29. package/dist/index.js +2361 -2058
  30. package/dist/index.mjs +2336 -2044
  31. package/dist/node.js +7 -7
  32. package/dist/node.mjs +1 -1
  33. package/dist/{sleep-BbYwFLG6.mjs → sleep-7WqNZVcL.mjs} +0 -1
  34. package/dist/{sleep-CyN9nHr_.js → sleep-ocMLM2o5.js} +0 -1
  35. package/package.json +1 -1
  36. package/dist/device/device-link-transform.d.ts +0 -5
  37. package/dist/{fmp4-box-splitter-BkWH7O3L.js → canonical-hash-DNV8S5ET.js} +33 -33
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as EventCategory } from "./event-category-41fKf-q9.mjs";
2
- import { $ as FrameHandleSchema, A as method, B as readinessKey, C as DeviceType, Ct as collectHydratedFieldEntries, D as event, E as DEVICE_STATUS_METHOD, Et as resolveHydratedFieldValue, F as readNodePin, G as CamProfileSchema, H as BrokerStatsSchema, I as toNodeId, J as CameraStreamSchema, K as CamStreamKindSchema, L as ReadinessRegistry, M as systemMethod, N as CAP_NODE_PIN_CONTEXT_KEY, O as expandCapMethods, P as nodePin, Q as FrameHandleFormatSchema, R as ReadinessTimeoutError, S as DeviceRole, St as WELL_KNOWN_TAB_MAP, T as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Tt as hydrateSchema, U as BrokerStatusSchema, V as scopeKey, W as CAM_PROFILE_ORDER, X as DecodedFrameSchema, Y as DecodedAudioChunkSchema, Z as EncodedPacketSchema, _ as RawStateResultSchema, _t as createDurableState, a as asJsonObject, at as SubscribeAudioChunksInputSchema, b as ChargingStatus, bt as isEvent, c as parseJsonArray, ct as SubscribeFramesResultSchema, d as DEVICE_SCOPED_CAPS, dt as parseProfileBrokerId, et as ProfileRtspEntrySchema, f as isDeviceScopedCap, ft as selectAssignedProfileSlots, g as createSliceHandle, gt as normalizeAddonInitResult, h as createMirrorSource, ht as BaseAddon, i as asJsonArray, it as StreamSourceSchema, j as resolveCapMount, k as isDeviceConfigCap, l as parseJsonObject, lt as makeProfileBrokerId, m as createLazyTrpcSource, mt as DisposerChain, n as sleepCancellable, nt as ProfileSlotStatusSchema, o as asNumber, ot as SubscribeAudioChunksResultSchema, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, q as CamStreamResolutionSchema, r as asBoolean, rt as StreamSourceEntrySchema, s as asString, st as SubscribeFramesInputSchema, t as sleep, tt as ProfileSlotSchema, u as parseJsonUnknown, ut as makeSourceBrokerId, v as deviceOpsCapability, vt as createEvent, w as adminUiCapability, wt as collectHydratedFieldValues, x as DeviceFeature, xt as WELL_KNOWN_TABS, y as viewerUiCapability, yt as emitReadiness, z as emitDownForOwnedCaps } from "./sleep-BbYwFLG6.mjs";
3
- import { a as buildAudioArgs, c as buildVideoArgs, d as logBannerArgs, f as pickVideoEncoder, i as audioPlanFromEncodeProfile, l as invocationFromEncodeProfile, n as canonicalHash, o as buildFfmpegArgs, r as AUDIO_PRESETS, s as buildInputArgs, t as Fmp4BoxSplitter, u as isSoftwareDecode } from "./fmp4-box-splitter-B53u9-Nu.mjs";
2
+ import { $ as FrameHandleSchema, A as method, B as readinessKey, C as DeviceType, Ct as collectHydratedFieldEntries, D as event, E as DEVICE_STATUS_METHOD, Et as resolveHydratedFieldValue, F as readNodePin, G as CamProfileSchema, H as BrokerStatsSchema, I as toNodeId, J as CameraStreamSchema, K as CamStreamKindSchema, L as ReadinessRegistry, M as systemMethod, N as CAP_NODE_PIN_CONTEXT_KEY, O as expandCapMethods, P as nodePin, Q as FrameHandleFormatSchema, R as ReadinessTimeoutError, S as DeviceRole, St as WELL_KNOWN_TAB_MAP, T as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Tt as hydrateSchema, U as BrokerStatusSchema, V as scopeKey, W as CAM_PROFILE_ORDER, X as DecodedFrameSchema, Y as DecodedAudioChunkSchema, Z as EncodedPacketSchema, _ as RawStateResultSchema, _t as createDurableState, a as asJsonObject, at as SubscribeAudioChunksInputSchema, b as ChargingStatus, bt as isEvent, c as parseJsonArray, ct as SubscribeFramesResultSchema, d as DEVICE_SCOPED_CAPS, dt as parseProfileBrokerId, et as ProfileRtspEntrySchema, f as isDeviceScopedCap, ft as selectAssignedProfileSlots, g as createSliceHandle, gt as normalizeAddonInitResult, h as createMirrorSource, ht as BaseAddon, i as asJsonArray, it as StreamSourceSchema, j as resolveCapMount, k as isDeviceConfigCap, l as parseJsonObject, lt as makeProfileBrokerId, m as createLazyTrpcSource, mt as DisposerChain, n as sleepCancellable, nt as ProfileSlotStatusSchema, o as asNumber, ot as SubscribeAudioChunksResultSchema, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, q as CamStreamResolutionSchema, r as asBoolean, rt as StreamSourceEntrySchema, s as asString, st as SubscribeFramesInputSchema, t as sleep, tt as ProfileSlotSchema, u as parseJsonUnknown, ut as makeSourceBrokerId, v as deviceOpsCapability, vt as createEvent, w as adminUiCapability, wt as collectHydratedFieldValues, x as DeviceFeature, xt as WELL_KNOWN_TABS, y as viewerUiCapability, yt as emitReadiness, z as emitDownForOwnedCaps } from "./sleep-7WqNZVcL.mjs";
3
+ import { a as buildAudioArgs, c as buildVideoArgs, d as logBannerArgs, f as pickVideoEncoder, i as audioPlanFromEncodeProfile, l as invocationFromEncodeProfile, n as Fmp4BoxSplitter, o as buildFfmpegArgs, r as AUDIO_PRESETS, s as buildInputArgs, t as canonicalHash, u as isSoftwareDecode } from "./canonical-hash-rO1sRmEK.mjs";
4
4
  import { EventSourceType } from "./enums.mjs";
5
5
  import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
6
6
  import { z } from "zod";
@@ -4325,12 +4325,34 @@ var streamBrokerCapability = {
4325
4325
  kinds: z.array(EventMediaKindSchema).min(1).default(["mp4"]),
4326
4326
  /** GIF geometry. The video keeps the source's own. */
4327
4327
  gifMaxWidth: z.number().int().min(120).max(1280).default(640),
4328
- gifFps: z.number().int().min(1).max(15).default(8),
4329
4328
  /**
4330
- * Playback rate, applied to EVERY container so they stay one clip.
4331
- * `1` is real time and is what allows the copy branch.
4329
+ * The gif's own PLAYBACK rate in frames per second what the finished
4330
+ * gif runs at, not how many source frames feed it. The decimation that
4331
+ * feeds it samples `gifFps / gifSpeed` source frames per second, so at
4332
+ * the defaults a 12 fps gif is built out of 3 source frames a second.
4332
4333
  */
4333
- speed: z.number().min(1).max(8).default(1)
4334
+ gifFps: z.number().int().min(1).max(15).default(12),
4335
+ /**
4336
+ * How fast the GIF plays against real time, independent of `speed`.
4337
+ *
4338
+ * 4× by default, by operator request: a notification gif is glanced at
4339
+ * on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
4340
+ * a separate knob from `speed` even though both now default to 4 —
4341
+ * a caller wanting a real-time video and a fast gif must not have to
4342
+ * choose.
4343
+ */
4344
+ gifSpeed: z.number().min(1).max(8).default(4),
4345
+ /**
4346
+ * Playback rate of the VIDEO. Also 4× by default, by operator decision.
4347
+ *
4348
+ * `1` is real time and is the ONLY value that allows the copy branch —
4349
+ * anything else forces `libx264` over the window. That was priced
4350
+ * before it was chosen: a per-event burst measured at 0.23 s and 254 KB
4351
+ * on a real 615 720p cut, against 922 KB for the copy it replaces. A
4352
+ * re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
4353
+ * once the decode is forced the width stops being free.
4354
+ */
4355
+ speed: z.number().min(1).max(8).default(4)
4334
4356
  }), EventMediaProductionSchema, {
4335
4357
  kind: "mutation",
4336
4358
  auth: "admin"
@@ -4927,6 +4949,28 @@ function kebabToCamel(s) {
4927
4949
  * instead of taking the hub with it (D6). Every method here is admin-only, and
4928
4950
  * must stay so.
4929
4951
  */
4952
+ /**
4953
+ * Prefix for a block's generated addon id AND its runner id — one addon, one
4954
+ * process, so the two are the same string (D2).
4955
+ *
4956
+ * It lives on the CONTRACT rather than beside the code that materialises the
4957
+ * package, because it is not an implementation detail: the operator surface
4958
+ * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
4959
+ * with no cap and no store of its own. Two copies of this string is one copy
4960
+ * too many — a drift would show as a logs pane that is simply always empty.
4961
+ */
4962
+ var CORE_BLOCK_ADDON_PREFIX = "core-block-";
4963
+ /** The addon/runner id a block's process runs under. */
4964
+ function coreBlockAddonId(blockId) {
4965
+ return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
4966
+ }
4967
+ /** The block id behind a generated addon/runner id, or null when the id belongs
4968
+ * to something else. The inverse of {@link coreBlockAddonId}. */
4969
+ function coreBlockIdFromAddonId(addonId) {
4970
+ if (!addonId.startsWith("core-block-")) return null;
4971
+ const rest = addonId.slice(11);
4972
+ return rest.length > 0 ? rest : null;
4973
+ }
4930
4974
  /** Where a block runs. The operator chooses — a block driving a device on an
4931
4975
  * agent is the reason placement is not fixed to the hub. */
4932
4976
  var CoreBlockPlacementSchema = z.union([z.literal("hub"), z.string().min(1)]);
@@ -5015,6 +5059,23 @@ var coreBlocksCapability = {
5015
5059
  auth: "admin"
5016
5060
  }),
5017
5061
  /**
5062
+ * Stop this block's runner and let the supervisor bring it back.
5063
+ *
5064
+ * **It stores nothing.** There is no "restart requested" flag and no second
5065
+ * lifecycle field: the supervisor already stops and respawns a runner when
5066
+ * the code hash changes, and this exposes that same path deliberately
5067
+ * (D62 — a switch writes the authority that already owned the function).
5068
+ * The next reconcile pass remains the authority, so a restart that lost a
5069
+ * race is corrected within one pass rather than leaving a stale flag behind.
5070
+ *
5071
+ * Refused for a DISABLED block: nothing would come back, and a button that
5072
+ * silently does nothing is worse than one that says why.
5073
+ */
5074
+ restart: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema }), {
5075
+ kind: "mutation",
5076
+ auth: "admin"
5077
+ }),
5078
+ /**
5018
5079
  * Type-check without saving — what the editor calls as the author types, so
5019
5080
  * the compiler's verdict is the same one the server will reach.
5020
5081
  */
@@ -6230,1114 +6291,247 @@ var deviceExportCapability = {
6230
6291
  }
6231
6292
  };
6232
6293
  //#endregion
6233
- //#region src/expression/limits.ts
6294
+ //#region src/capabilities/device-provider.cap.ts
6295
+ var ProviderStatusSchema = z.object({
6296
+ connected: z.boolean(),
6297
+ deviceCount: z.number(),
6298
+ error: z.string().optional()
6299
+ });
6300
+ var DiscoveredDeviceSchema = z.object({
6301
+ externalId: z.string(),
6302
+ name: z.string(),
6303
+ type: z.string(),
6304
+ metadata: z.record(z.string(), z.unknown()).optional()
6305
+ });
6234
6306
  /**
6235
- * Resource-bound constants for the safe expression engine.
6236
- *
6237
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
6238
- * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
6239
- * O(nodeCount) by construction. These caps merely put a hard ceiling on the
6240
- * work a single author-supplied expression can request, so a hostile or
6241
- * accidental pathological string can never spend unbounded CPU/memory.
6307
+ * Candidate handed back from discovery and accepted by
6308
+ * `adoptDiscoveredDevice`. Shape mirrors the in-process
6309
+ * `DiscoveredDevice` interface used by `DeviceDiscovery`.
6242
6310
  */
6243
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
6244
- * rejected without allocation. */
6245
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
6246
- /** Max AST nodes — checked during parse; a deeply nested grouping that exceeds
6247
- * this is rejected as "expression too complex". */
6248
- var MAX_EXPRESSION_AST_NODES = 256;
6249
- /** Defense-in-depth walker step budget one increment per node visit during
6250
- * evaluation. The grammar guarantees O(nodeCount) walks, so this can only trip
6251
- * on a crafted maximum-size AST. */
6252
- var MAX_EXPRESSION_EVAL_STEPS = 4096;
6253
- /** Max named bindings on one `DeviceLinkExpressionSource`. */
6254
- var MAX_EXPRESSION_BINDINGS = 32;
6255
- /** Max positional arguments to any builtin call. */
6256
- var MAX_EXPRESSION_CALL_ARGS = 16;
6257
- /** LRU compile-cache capacity (parsed ASTs keyed by raw source string). */
6258
- var EXPRESSION_COMPILE_CACHE_CAPACITY = 256;
6259
- /** A legal binding / identifier name. */
6260
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
6261
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
6262
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
6263
- var RESERVED_BINDING_NAMES = new Set([
6264
- "now",
6265
- "true",
6266
- "false",
6267
- "null"
6268
- ]);
6269
- //#endregion
6270
- //#region src/expression/errors.ts
6311
+ var DiscoveryCandidateSchema = z.object({
6312
+ stableId: z.string(),
6313
+ type: z.enum(DeviceType),
6314
+ suggestedName: z.string(),
6315
+ prefilledConfig: z.record(z.string(), z.unknown()),
6316
+ /**
6317
+ * Optional upstream-system identity (HA entity_id, vendor MAC, …).
6318
+ * Discovery pre-populates this for systems that know the upstream
6319
+ * identity ahead of adoption. Rendering metadata (unit, precision)
6320
+ * flows live through the cap STATUS SLICE after adoption.
6321
+ */
6322
+ sourceInfo: SourceInfoSchema.optional()
6323
+ });
6271
6324
  /**
6272
- * Error types for the safe expression engine. Two distinct classes so callers
6273
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
6274
- * failure both are non-fatal to the host: read paths degrade to "skip link".
6325
+ * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
6326
+ * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
6327
+ * tRPC layer can pass it through without reshaping.
6275
6328
  */
6276
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
6277
- * the failure is anchored to a character (author-facing inline feedback). */
6278
- var ExpressionParseError = class extends Error {
6279
- position;
6280
- constructor(message, position) {
6281
- super(message);
6282
- this.name = "ExpressionParseError";
6283
- this.position = position;
6284
- }
6285
- };
6286
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
6287
- * result, unknown builtin, step-budget exceeded). */
6288
- var ExpressionEvalError = class extends Error {
6289
- constructor(message) {
6290
- super(message);
6291
- this.name = "ExpressionEvalError";
6292
- }
6293
- };
6294
- //#endregion
6295
- //#region src/expression/builtins.ts
6329
+ var DeviceSummarySchema = z.object({
6330
+ id: z.number(),
6331
+ stableId: z.string(),
6332
+ addonId: z.string(),
6333
+ type: z.string(),
6334
+ name: z.string(),
6335
+ parentDeviceId: z.number().nullable(),
6336
+ online: z.boolean(),
6337
+ features: z.array(z.string()),
6338
+ config: z.record(z.string(), z.unknown()),
6339
+ /** Optional upstream-system identity (dispatch key + system tag).
6340
+ * See `SourceInfo`. Present when the device has a non-synthetic
6341
+ * source identifier (HA entities, vendor MAC, …); omitted when the
6342
+ * synthetic backfill is in effect. */
6343
+ sourceInfo: SourceInfoSchema.optional()
6344
+ });
6296
6345
  /**
6297
- * Frozen, null-prototype builtin function table for the expression engine
6298
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
6299
- * parser rejects any callee not in it, and the evaluator gates each call on an
6300
- * own-property check against it.
6301
- *
6302
- * Because the object has a NULL prototype AND is `Object.freeze`d:
6303
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
6304
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
6305
- * (there is no `Object.prototype` in the chain), so those names are not
6306
- * callable — they are simply "unknown function" at parse time.
6307
- *
6308
- * Every numeric argument is validated as a finite number and every numeric
6309
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
6310
- * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
6311
- * closed rather than emitting a garbage value.
6346
+ * Result of a live field test (e.g. probing an RTSP URL during device
6347
+ * creation). Matches the UI-side `FieldProbeResult` in
6348
+ * `interfaces/config-ui.ts` the admin `FormBuilder` renders the
6349
+ * returned `labels` as chips next to the input.
6312
6350
  */
6313
- function asFiniteNumber(value, name, index) {
6314
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
6315
- return value;
6316
- }
6317
- function asString$1(value, name, index) {
6318
- if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
6319
- return value;
6320
- }
6321
- function finiteResult(value, name) {
6322
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
6323
- return value;
6324
- }
6325
- function allFiniteNumbers(args, name) {
6326
- return args.map((a, idx) => asFiniteNumber(a, name, idx));
6327
- }
6328
- var INF = Number.POSITIVE_INFINITY;
6329
- var table = {
6330
- min: {
6331
- minArgs: 1,
6332
- maxArgs: INF,
6333
- apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
6334
- },
6335
- max: {
6336
- minArgs: 1,
6337
- maxArgs: INF,
6338
- apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
6339
- },
6340
- abs: {
6341
- minArgs: 1,
6342
- maxArgs: 1,
6343
- apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
6344
- },
6345
- floor: {
6346
- minArgs: 1,
6347
- maxArgs: 1,
6348
- apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
6349
- },
6350
- ceil: {
6351
- minArgs: 1,
6352
- maxArgs: 1,
6353
- apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
6354
- },
6355
- sqrt: {
6356
- minArgs: 1,
6357
- maxArgs: 1,
6358
- apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
6359
- },
6360
- round: {
6361
- minArgs: 1,
6362
- maxArgs: 2,
6363
- apply: (args) => {
6364
- const x = asFiniteNumber(args[0], "round", 0);
6365
- const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
6366
- if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
6367
- const factor = 10 ** digits;
6368
- return finiteResult(Math.round(x * factor) / factor, "round");
6369
- }
6370
- },
6371
- pow: {
6372
- minArgs: 2,
6373
- maxArgs: 2,
6374
- apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
6375
- },
6376
- clamp: {
6377
- minArgs: 3,
6378
- maxArgs: 3,
6379
- apply: (args) => {
6380
- const x = asFiniteNumber(args[0], "clamp", 0);
6381
- const lo = asFiniteNumber(args[1], "clamp", 1);
6382
- const hi = asFiniteNumber(args[2], "clamp", 2);
6383
- if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
6384
- return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
6385
- }
6386
- },
6387
- avg: {
6388
- minArgs: 1,
6389
- maxArgs: INF,
6390
- apply: (args) => {
6391
- const nums = allFiniteNumbers(args, "avg");
6392
- return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
6393
- }
6394
- },
6395
- sum: {
6396
- minArgs: 1,
6397
- maxArgs: INF,
6398
- apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
6399
- },
6400
- coalesce: {
6401
- minArgs: 1,
6402
- maxArgs: INF,
6403
- apply: (args) => {
6404
- for (const a of args) if (a !== null) return a;
6405
- return null;
6406
- }
6407
- },
6408
- age: {
6409
- minArgs: 2,
6410
- maxArgs: 2,
6411
- apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
6412
- },
6413
- convert: {
6414
- minArgs: 3,
6415
- maxArgs: 3,
6416
- apply: (args, hooks) => {
6417
- const x = asFiniteNumber(args[0], "convert", 0);
6418
- const from = asString$1(args[1], "convert", 1).trim();
6419
- const to = asString$1(args[2], "convert", 2).trim();
6420
- if (hooks.convert) {
6421
- const out = hooks.convert(x, from, to);
6422
- if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
6423
- return finiteResult(out, "convert");
6424
- }
6425
- if (from === to) return x;
6426
- throw new ExpressionEvalError("convert: unit conversion table not installed");
6427
- }
6428
- }
6429
- };
6430
- /** Frozen, null-prototype builtin table. */
6431
- var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
6432
- /** The set of valid builtin names — used by the parser to reject unknown
6433
- * callees at parse time (immediate author feedback). */
6434
- var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
6435
- //#endregion
6436
- //#region src/expression/tokenizer.ts
6351
+ var FieldProbeResultSchema = z.object({
6352
+ status: z.enum(["ok", "error"]),
6353
+ labels: z.array(z.string()).optional(),
6354
+ error: z.string().optional()
6355
+ });
6437
6356
  /**
6438
- * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
6439
- * zero-dependency. The grammar is deliberately boring: decimal numbers,
6440
- * single/double-quoted strings with a tiny escape set, identifiers, the three
6441
- * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
6442
- * outside that a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
6443
- * is a parse error with a source position, so member access / assignment /
6444
- * template literals are lexically impossible.
6357
+ * The output of `getChildCreationSchema` is a UI schema tree. We store
6358
+ * it as `unknown` at the capability layer — the router just passes it
6359
+ * through and the admin UI renders it via `FormBuilder`. The actual
6360
+ * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
6361
+ * but we deliberately avoid a Zod mirror because the union is large and
6362
+ * not meant for runtime validation at this seam.
6445
6363
  */
6446
- var KEYWORDS = new Set([
6447
- "true",
6448
- "false",
6449
- "null"
6450
- ]);
6451
- function isDigit(ch) {
6452
- return ch >= "0" && ch <= "9";
6453
- }
6454
- function isIdentStart(ch) {
6455
- return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
6456
- }
6457
- function isIdentPart(ch) {
6458
- return isIdentStart(ch) || isDigit(ch);
6459
- }
6460
- function isWhitespace(ch) {
6461
- return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
6462
- }
6463
- /** Tokenize `source` into a flat token list ending with a single `eof` token.
6464
- * Throws `ExpressionParseError` on any illegal character or unterminated
6465
- * string. */
6466
- function tokenize(source) {
6467
- if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
6468
- const tokens = [];
6469
- let i = 0;
6470
- const n = source.length;
6471
- while (i < n) {
6472
- const ch = source[i];
6473
- if (isWhitespace(ch)) {
6474
- i += 1;
6475
- continue;
6476
- }
6477
- if (isDigit(ch)) {
6478
- const start = i;
6479
- while (i < n && isDigit(source[i])) i += 1;
6480
- if (i < n && source[i] === ".") {
6481
- if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
6482
- i += 1;
6483
- while (i < n && isDigit(source[i])) i += 1;
6484
- }
6485
- const text = source.slice(start, i);
6486
- const value = Number(text);
6487
- if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
6488
- tokens.push({
6489
- type: "number",
6490
- value,
6491
- pos: start
6492
- });
6493
- continue;
6494
- }
6495
- if (ch === "'" || ch === "\"") {
6496
- const quote = ch;
6497
- const start = i;
6498
- i += 1;
6499
- let out = "";
6500
- let closed = false;
6501
- while (i < n) {
6502
- const c = source[i];
6503
- if (c === "\\") {
6504
- const next = i + 1 < n ? source[i + 1] : "";
6505
- if (next === "\\" || next === "'" || next === "\"") {
6506
- out += next;
6507
- i += 2;
6508
- continue;
6509
- }
6510
- throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
6511
- }
6512
- if (c === quote) {
6513
- closed = true;
6514
- i += 1;
6515
- break;
6516
- }
6517
- out += c;
6518
- i += 1;
6519
- }
6520
- if (!closed) throw new ExpressionParseError("unterminated string literal", start);
6521
- tokens.push({
6522
- type: "string",
6523
- value: out,
6524
- pos: start
6525
- });
6526
- continue;
6527
- }
6528
- if (isIdentStart(ch)) {
6529
- const start = i;
6530
- while (i < n && isIdentPart(source[i])) i += 1;
6531
- const text = source.slice(start, i);
6532
- if (KEYWORDS.has(text)) tokens.push({
6533
- type: "keyword",
6534
- keyword: keywordOf(text),
6535
- pos: start
6536
- });
6537
- else tokens.push({
6538
- type: "identifier",
6539
- name: text,
6540
- pos: start
6541
- });
6542
- continue;
6543
- }
6544
- const two = i + 1 < n ? source.slice(i, i + 2) : "";
6545
- if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
6546
- tokens.push({
6547
- type: "punct",
6548
- punct: two,
6549
- pos: i
6550
- });
6551
- i += 2;
6552
- continue;
6553
- }
6554
- if (isSinglePunct(ch)) {
6555
- tokens.push({
6556
- type: "punct",
6557
- punct: ch,
6558
- pos: i
6559
- });
6560
- i += 1;
6561
- continue;
6562
- }
6563
- throw new ExpressionParseError(`unexpected character '${ch}'`, i);
6364
+ var CreationSchemaOutputSchema = z.unknown();
6365
+ var deviceProviderCapability = {
6366
+ name: "device-provider",
6367
+ scope: "system",
6368
+ mode: "collection",
6369
+ methods: {
6370
+ start: method(z.void(), z.void(), { kind: "mutation" }),
6371
+ stop: method(z.void(), z.void(), { kind: "mutation" }),
6372
+ getStatus: method(z.void(), ProviderStatusSchema),
6373
+ getDevices: method(z.void(), z.array(z.object({
6374
+ id: z.string(),
6375
+ name: z.string(),
6376
+ type: z.string()
6377
+ }))),
6378
+ supportsDiscovery: method(z.object({}), z.boolean()),
6379
+ /**
6380
+ * Run a network scan. `params` carries optional provider-specific scan
6381
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
6382
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
6383
+ * (provider uses its local-network default).
6384
+ */
6385
+ discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
6386
+ kind: "mutation",
6387
+ auth: "admin"
6388
+ }),
6389
+ /**
6390
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
6391
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
6392
+ * `null` when the provider takes no extra scan params — the generic
6393
+ * aggregated scan never renders this; the per-integration scan does.
6394
+ */
6395
+ getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
6396
+ /**
6397
+ * The DeviceType this provider creates via manual add (Camera for
6398
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
6399
+ * provider does not support manual creation. Lets the Add-Device dialog
6400
+ * pick the right type instead of assuming Camera.
6401
+ */
6402
+ getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
6403
+ adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
6404
+ kind: "mutation",
6405
+ auth: "admin"
6406
+ }),
6407
+ supportsManualCreation: method(z.object({}), z.boolean()),
6408
+ /**
6409
+ * Fetch the creation form schema for a given DeviceType. Returns
6410
+ * `null` when the provider does not support manually creating
6411
+ * devices of that type. The output is a `ConfigUISchema` — the
6412
+ * router type-asserts it at the boundary.
6413
+ */
6414
+ getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
6415
+ createDevice: method(z.object({
6416
+ type: z.enum(DeviceType),
6417
+ config: z.record(z.string(), z.unknown())
6418
+ }), DeviceSummarySchema, {
6419
+ kind: "mutation",
6420
+ auth: "admin"
6421
+ }),
6422
+ /**
6423
+ * Test a single field in the creation form before the device has
6424
+ * been persisted. Typical use: probing an RTSP URL entered by the
6425
+ * user. Providers that don't support field probing return
6426
+ * `{ success: true, message: 'Field test not supported' }`.
6427
+ *
6428
+ * `formValues` is the live snapshot of every field in the form at
6429
+ * the moment the user clicked Test — useful for probes that depend
6430
+ * on multiple fields together (e.g. Reolink autodetect needs host
6431
+ * + credentials + UID + transport mode in a single call). Optional
6432
+ * for backwards compatibility; providers free to ignore it.
6433
+ */
6434
+ testCreationField: method(z.object({
6435
+ type: z.enum(DeviceType),
6436
+ key: z.string(),
6437
+ value: z.unknown(),
6438
+ formValues: z.record(z.string(), z.unknown()).optional()
6439
+ }), FieldProbeResultSchema, {
6440
+ kind: "mutation",
6441
+ auth: "admin"
6442
+ })
6564
6443
  }
6565
- tokens.push({
6566
- type: "eof",
6567
- pos: n
6568
- });
6569
- return tokens;
6570
- }
6571
- function keywordOf(text) {
6572
- if (text === "true") return "true";
6573
- if (text === "false") return "false";
6574
- return "null";
6575
- }
6576
- function isSinglePunct(ch) {
6577
- return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
6578
- }
6444
+ };
6579
6445
  //#endregion
6580
- //#region src/expression/parser.ts
6446
+ //#region src/capabilities/device-manager.cap.ts
6581
6447
  /**
6582
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
6448
+ * Device Manager capability — hub-side singleton that unifies device persistence,
6449
+ * live registry access, and all management operations into a single tRPC surface.
6583
6450
  *
6584
- * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
6585
- * relational additive multiplicative → unary `! -` → call / primary.
6586
- * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
6587
- * string validated against the builtin table at parse time, so an unknown
6588
- * function is rejected immediately (author feedback) and a persisted expression
6589
- * that references a since-removed builtin degrades at read.
6451
+ * Replaces:
6452
+ * - `device-persistence` capability (persistence methods absorbed here)
6453
+ * - `device-management.router.ts` (deleted in Phase 2)
6454
+ * - `device-ops.router.ts` (compat layer deleted; device-provider ops absorbed here)
6590
6455
  *
6591
- * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
6592
- * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) both raise `ExpressionParseError`.
6593
- */
6594
- /** Binary/logical operator precedence (higher binds tighter). */
6595
- var BINARY_PRECEDENCE = {
6596
- "||": 1,
6597
- "&&": 2,
6598
- "==": 3,
6599
- "!=": 3,
6600
- "<": 4,
6601
- "<=": 4,
6602
- ">": 4,
6603
- ">=": 4,
6604
- "+": 5,
6605
- "-": 5,
6606
- "*": 6,
6607
- "/": 6,
6608
- "%": 6
6609
- };
6610
- function isLogicalOp(op) {
6611
- return op === "&&" || op === "||";
6612
- }
6613
- function isBinaryOp(op) {
6614
- return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
6615
- }
6616
- var Parser = class {
6617
- tokens;
6618
- pos = 0;
6619
- nodeCount = 0;
6620
- identifiers = /* @__PURE__ */ new Set();
6621
- callees = /* @__PURE__ */ new Set();
6622
- constructor(tokens) {
6623
- this.tokens = tokens;
6624
- }
6625
- parse() {
6626
- const ast = this.parseTernary();
6627
- const tok = this.peek();
6628
- if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
6629
- return {
6630
- ast,
6631
- identifiers: this.identifiers,
6632
- callees: this.callees,
6633
- nodeCount: this.nodeCount
6634
- };
6635
- }
6636
- peek() {
6637
- return this.tokens[this.pos];
6638
- }
6639
- next() {
6640
- return this.tokens[this.pos++];
6641
- }
6642
- /** Consume a punctuator token, erroring if the next token isn't it. */
6643
- expectPunct(punct) {
6644
- const tok = this.peek();
6645
- if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
6646
- this.pos += 1;
6647
- }
6648
- matchPunct(punct) {
6649
- const tok = this.peek();
6650
- if (tok.type === "punct" && tok.punct === punct) {
6651
- this.pos += 1;
6652
- return true;
6653
- }
6654
- return false;
6655
- }
6656
- countNode() {
6657
- this.nodeCount += 1;
6658
- if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
6659
- }
6660
- parseTernary() {
6661
- const test = this.parseBinary(1);
6662
- if (this.matchPunct("?")) {
6663
- const consequent = this.parseTernary();
6664
- this.expectPunct(":");
6665
- const alternate = this.parseTernary();
6666
- this.countNode();
6667
- return {
6668
- kind: "conditional",
6669
- test,
6670
- consequent,
6671
- alternate
6672
- };
6673
- }
6674
- return test;
6675
- }
6676
- parseBinary(minPrec) {
6677
- let left = this.parseUnary();
6678
- for (;;) {
6679
- const tok = this.peek();
6680
- if (tok.type !== "punct") break;
6681
- const prec = BINARY_PRECEDENCE[tok.punct];
6682
- if (prec === void 0 || prec < minPrec) break;
6683
- const op = tok.punct;
6684
- this.pos += 1;
6685
- const right = this.parseBinary(prec + 1);
6686
- this.countNode();
6687
- if (isLogicalOp(op)) left = {
6688
- kind: "logical",
6689
- op,
6690
- left,
6691
- right
6692
- };
6693
- else if (isBinaryOp(op)) left = {
6694
- kind: "binary",
6695
- op,
6696
- left,
6697
- right
6698
- };
6699
- else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
6700
- }
6701
- return left;
6702
- }
6703
- parseUnary() {
6704
- const tok = this.peek();
6705
- if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
6706
- const op = tok.punct;
6707
- this.pos += 1;
6708
- const operand = this.parseUnary();
6709
- this.countNode();
6710
- return {
6711
- kind: "unary",
6712
- op,
6713
- operand
6714
- };
6715
- }
6716
- return this.parsePrimary();
6717
- }
6718
- parsePrimary() {
6719
- const tok = this.next();
6720
- switch (tok.type) {
6721
- case "number":
6722
- this.countNode();
6723
- return {
6724
- kind: "literal",
6725
- value: tok.value
6726
- };
6727
- case "string":
6728
- this.countNode();
6729
- return {
6730
- kind: "literal",
6731
- value: tok.value
6732
- };
6733
- case "keyword":
6734
- this.countNode();
6735
- return {
6736
- kind: "literal",
6737
- value: tok.keyword === "null" ? null : tok.keyword === "true"
6738
- };
6739
- case "identifier": {
6740
- const nextTok = this.peek();
6741
- if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
6742
- this.identifiers.add(tok.name);
6743
- this.countNode();
6744
- return {
6745
- kind: "identifier",
6746
- name: tok.name
6747
- };
6748
- }
6749
- case "punct":
6750
- if (tok.punct === "(") {
6751
- const inner = this.parseTernary();
6752
- this.expectPunct(")");
6753
- return inner;
6754
- }
6755
- throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
6756
- case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
6757
- }
6758
- }
6759
- parseCall(callee, pos) {
6760
- if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
6761
- this.expectPunct("(");
6762
- const args = [];
6763
- if (!this.matchPunct(")")) for (;;) {
6764
- args.push(this.parseTernary());
6765
- if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
6766
- if (this.matchPunct(",")) continue;
6767
- this.expectPunct(")");
6768
- break;
6769
- }
6770
- this.callees.add(callee);
6771
- this.countNode();
6772
- return {
6773
- kind: "call",
6774
- callee,
6775
- args
6776
- };
6777
- }
6778
- };
6779
- /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
6780
- * `ExpressionParseError` on any lexical or grammatical failure. */
6781
- function parseExpression(source) {
6782
- return new Parser(tokenize(source)).parse();
6783
- }
6784
- //#endregion
6785
- //#region src/expression/compile.ts
6786
- /**
6787
- * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
6788
- * by expr"). The cache stores BOTH successes and failures (negative caching),
6789
- * so a corrupt persisted string costs exactly one tokenize+parse total — not
6790
- * one per read on a hot resolve path.
6791
- *
6792
- * The cache is a module-level singleton: entries are pure, content-addressed
6793
- * ASTs keyed by the raw source string, so sharing one instance across all
6794
- * callers is safe and maximises hit rate.
6795
- */
6796
- var cache = /* @__PURE__ */ new Map();
6797
- function getCached(source) {
6798
- const hit = cache.get(source);
6799
- if (hit !== void 0) {
6800
- cache.delete(source);
6801
- cache.set(source, hit);
6802
- return hit;
6803
- }
6804
- let result;
6805
- try {
6806
- result = {
6807
- ok: true,
6808
- parsed: parseExpression(source)
6809
- };
6810
- } catch (err) {
6811
- result = {
6812
- ok: false,
6813
- error: err instanceof ExpressionParseError ? err.message : String(err)
6814
- };
6815
- }
6816
- cache.set(source, result);
6817
- if (cache.size > 256) {
6818
- const oldest = cache.keys().next().value;
6819
- if (oldest !== void 0) cache.delete(oldest);
6820
- }
6821
- return result;
6822
- }
6823
- /** Compile `source` to a `ParsedExpression`, throwing `ExpressionParseError`
6824
- * on failure. LRU/negative-cached. */
6825
- function compileExpression(source) {
6826
- const result = getCached(source);
6827
- if (result.ok) return result.parsed;
6828
- throw new ExpressionParseError(result.error);
6829
- }
6830
- /** Compile `source`, returning a discriminated result instead of throwing.
6831
- * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
6832
- function compileExpressionSafe(source) {
6833
- return getCached(source);
6834
- }
6835
- //#endregion
6836
- //#region src/expression/evaluator.ts
6837
- /**
6838
- * Tree-walking evaluator for the safe expression mini-language.
6456
+ * All device provider addons (rtsp, onvif, frigate, ) are hub-local: they may
6457
+ * fork into separate processes but never run on remote cluster agents. Therefore:
6458
+ * - No nodeId routing needed — this is a pure hub singleton.
6459
+ * - The hub's DeviceRegistry is the single source of truth for all live devices.
6460
+ * - No shadow registry or cross-node aggregation required.
6839
6461
  *
6840
- * SECURITY (spec §4 rule 2/5):
6841
- * - The scope is an `Object.create(null)` copy of ONLY the caller's own
6842
- * enumerable binding entries, so `name in scope` is a pure own-key check and
6843
- * `constructor` / `__proto__` / `toString` are plain unknown identifiers.
6844
- * - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
6845
- * directly — the only external calls are into the frozen builtin table.
6846
- * - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
6847
- * construction; the step counter is defense-in-depth for a crafted max-size
6848
- * AST. Nothing blocks: there are no timers, awaits or unbounded loops.
6849
- */
6850
- var EMPTY_HOOKS = Object.freeze({});
6851
- /** Build a null-prototype scope from own-enumerable binding entries. Inherited
6852
- * keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
6853
- * so nothing smuggles in via the prototype chain. */
6854
- function createExpressionScope(bindings) {
6855
- const scope = Object.create(null);
6856
- for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
6857
- return scope;
6858
- }
6859
- function isFiniteNumber(value) {
6860
- return typeof value === "number" && Number.isFinite(value);
6861
- }
6862
- /** JS truthiness of a primitive value. */
6863
- function truthy(value) {
6864
- return Boolean(value);
6865
- }
6866
- function requireFinite(value, context) {
6867
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
6868
- return value;
6869
- }
6870
- function step(ctx) {
6871
- ctx.steps += 1;
6872
- if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
6873
- }
6874
- function evalNode(node, ctx) {
6875
- step(ctx);
6876
- switch (node.kind) {
6877
- case "literal": return node.value;
6878
- case "identifier":
6879
- if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
6880
- return ctx.scope[node.name];
6881
- case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
6882
- case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
6883
- case "logical": {
6884
- const left = evalNode(node.left, ctx);
6885
- if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
6886
- return truthy(left) ? left : evalNode(node.right, ctx);
6887
- }
6888
- case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
6889
- case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
6890
- }
6891
- }
6892
- function evalUnary(op, operand) {
6893
- if (op === "!") return !truthy(operand);
6894
- if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
6895
- return requireFinite(-operand, "unary \"-\"");
6896
- }
6897
- function evalBinary(op, left, right) {
6898
- switch (op) {
6899
- case "==": return left === right;
6900
- case "!=": return left !== right;
6901
- case "+":
6902
- if (typeof left === "string" && typeof right === "string") return left + right;
6903
- if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
6904
- throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
6905
- case "-":
6906
- case "*":
6907
- case "/":
6908
- case "%":
6909
- if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
6910
- return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
6911
- case "<":
6912
- case "<=":
6913
- case ">":
6914
- case ">=":
6915
- if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
6916
- if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
6917
- throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
6918
- }
6919
- }
6920
- function evalCall(callee, args, hooks) {
6921
- if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
6922
- const builtin = EXPRESSION_BUILTINS[callee];
6923
- if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
6924
- return builtin.apply(args, hooks);
6925
- }
6926
- /** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
6927
- * runtime failure (unknown identifier, type mismatch, non-finite result,
6928
- * step-budget exhaustion). */
6929
- function evaluateAst(node, scope, opts) {
6930
- return evalNode(node, {
6931
- scope,
6932
- hooks: opts?.hooks ?? EMPTY_HOOKS,
6933
- maxSteps: opts?.maxSteps ?? 4096,
6934
- steps: 0
6935
- });
6936
- }
6937
- //#endregion
6938
- //#region src/expression/link-expression.ts
6939
- /**
6940
- * DeviceLink-facing helpers for the expression engine — the single seam both
6941
- * resolver channels (async provider-read + sync mirror) and the wire-schema
6942
- * `superRefine` share, so validation and evaluation semantics stay identical
6943
- * everywhere.
6944
- */
6945
- /** The `now` epoch-ms binding is auto-injected into every evaluation and is a
6946
- * reserved binding name (authors may not rebind it). */
6947
- var EXPRESSION_INJECTED_NOW = "now";
6948
- /**
6949
- * Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
6950
- * Non-primitive values (objects, arrays, `undefined`, functions, bigint,
6951
- * symbol) and non-finite numbers become `undefined` so the caller can apply
6952
- * its binding-miss policy (→ `null`). `null` itself is a valid value.
6953
- */
6954
- function toExpressionValue(raw) {
6955
- if (raw === null) return null;
6956
- if (typeof raw === "string") return raw;
6957
- if (typeof raw === "boolean") return raw;
6958
- if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
6959
- }
6960
- /**
6961
- * Author-time validation. Returns `null` when the source is valid, else a
6962
- * human-readable error message. Checks: the expression compiles; binding count
6963
- * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
6964
- * is not reserved (`now`/keywords) and does not shadow a builtin; and every
6965
- * FREE identifier of the AST is covered by a binding or the injected `now`.
6966
- */
6967
- function validateExpressionSource(src) {
6968
- const names = Object.keys(src.bindings);
6969
- if (names.length > 32) return `too many bindings (${names.length} > 32)`;
6970
- for (const name of names) {
6971
- if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
6972
- if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
6973
- if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
6974
- }
6975
- const compiled = compileExpressionSafe(src.expr);
6976
- if (!compiled.ok) return compiled.error;
6977
- const bound = new Set(names);
6978
- for (const id of compiled.parsed.identifiers) {
6979
- if (id === "now") continue;
6980
- if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
6981
- }
6982
- return null;
6983
- }
6984
- /**
6985
- * Shared read-path evaluation for BOTH resolver channels. Builds a null-proto
6986
- * scope from `bindingValues` plus the injected `now` (supplied by the caller
6987
- * for determinism/testability), compiles via the LRU, and evaluates. Any
6988
- * failure (parse or eval) returns `{ ok: false }` — the caller treats that as
6989
- * "skip this link".
6462
+ * Forked workers register devices back to the hub via `ctx.devices`
6463
+ * (DeviceManagerApi ctx.api.deviceManager.registerDevice), same as today.
6990
6464
  */
6991
- function evaluateLinkExpression(expr, bindingValues, now, opts) {
6992
- const compiled = compileExpressionSafe(expr);
6993
- if (!compiled.ok) return {
6994
- ok: false,
6995
- error: compiled.error
6996
- };
6997
- const scope = createExpressionScope({
6998
- ...bindingValues,
6999
- ["now"]: now
7000
- });
7001
- try {
7002
- return {
7003
- ok: true,
7004
- value: evaluateAst(compiled.parsed.ast, scope, opts)
7005
- };
7006
- } catch (err) {
7007
- return {
7008
- ok: false,
7009
- error: err instanceof ExpressionEvalError ? err.message : String(err)
7010
- };
7011
- }
7012
- }
7013
- //#endregion
7014
- //#region src/capabilities/device-provider.cap.ts
7015
- var ProviderStatusSchema = z.object({
7016
- connected: z.boolean(),
7017
- deviceCount: z.number(),
7018
- error: z.string().optional()
6465
+ /** One child-placement directive on a container's `childLayout`. Structurally
6466
+ * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
6467
+ * shape for the same field. The child is identified by its re-sync-stable
6468
+ * accessory `stableIdSuffix` (`childKey`); listed children are grouped into
6469
+ * named accordion sections (with optional intra-section order). */
6470
+ var ChildLayoutEntrySchema = z.object({
6471
+ childKey: z.string(),
6472
+ section: z.string(),
6473
+ order: z.number().optional(),
6474
+ collapsed: z.boolean().optional()
7019
6475
  });
7020
- var DiscoveredDeviceSchema = z.object({
7021
- externalId: z.string(),
7022
- name: z.string(),
7023
- type: z.string(),
7024
- metadata: z.record(z.string(), z.unknown()).optional()
6476
+ /** Cap-wire shape of a per-cap display refinement — mirrors
6477
+ * `DeviceCapDisplayOverride` in `device-management.ts`. */
6478
+ var DeviceCapDisplayOverrideSchema = z.object({
6479
+ unit: z.string().min(1).optional(),
6480
+ precision: z.number().int().min(0).max(10).optional()
7025
6481
  });
7026
- /**
7027
- * Candidate handed back from discovery and accepted by
7028
- * `adoptDiscoveredDevice`. Shape mirrors the in-process
7029
- * `DiscoveredDevice` interface used by `DeviceDiscovery`.
7030
- */
7031
- var DiscoveryCandidateSchema = z.object({
7032
- stableId: z.string(),
7033
- type: z.enum(DeviceType),
7034
- suggestedName: z.string(),
7035
- prefilledConfig: z.record(z.string(), z.unknown()),
7036
- /**
7037
- * Optional upstream-system identity (HA entity_id, vendor MAC, …).
7038
- * Discovery pre-populates this for systems that know the upstream
7039
- * identity ahead of adoption. Rendering metadata (unit, precision)
7040
- * flows live through the cap STATUS SLICE after adoption.
7041
- */
7042
- sourceInfo: SourceInfoSchema.optional()
6482
+ /** Cap-wire shape of an operator-authored per-device display override —
6483
+ * mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
6484
+ * bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
6485
+ var DeviceDisplayOverrideSchema = z.object({
6486
+ icon: z.string().min(1).optional(),
6487
+ label: z.string().min(1).optional(),
6488
+ unit: z.string().min(1).optional(),
6489
+ precision: z.number().int().min(0).max(10).optional(),
6490
+ hidden: z.boolean().optional(),
6491
+ perCap: z.record(z.string(), DeviceCapDisplayOverrideSchema).optional()
6492
+ });
6493
+ /** Cap-wire shape of a per-role display default mirrors `RoleDisplayDefault`
6494
+ * in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
6495
+ * the wire as plain strings everywhere else cf. `DeviceInfoSchema.role`). */
6496
+ var RoleDisplayDefaultSchema = z.object({
6497
+ unit: z.string().min(1).optional(),
6498
+ precision: z.number().int().min(0).max(10).optional(),
6499
+ icon: z.string().min(1).optional()
7043
6500
  });
7044
6501
  /**
7045
- * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
7046
- * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
7047
- * tRPC layer can pass it through without reshaping.
6502
+ * Serializable projection of a live IDevice.
6503
+ * Returned by listAll, getDevice, getChildren.
6504
+ * Live methods (getStreamSources, getConfigSchema) are separate calls.
7048
6505
  */
7049
- var DeviceSummarySchema = z.object({
6506
+ var DeviceInfoSchema = z.object({
6507
+ /** Progressive, system-wide unique number. Allocated synchronously by
6508
+ * `device-manager.allocateDeviceId` BEFORE the owning `IDevice` is
6509
+ * constructed, so every live device exposes an `id` — no transient
6510
+ * null window. Distinct from `stableId`, which is unique per
6511
+ * integration. Ids are monotonic and never reissued on removal. */
7050
6512
  id: z.number(),
7051
6513
  stableId: z.string(),
7052
6514
  addonId: z.string(),
7053
- type: z.string(),
6515
+ type: z.enum(DeviceType),
7054
6516
  name: z.string(),
6517
+ /** Operator-organisational location label. `null` when unset. */
6518
+ location: z.string().nullable(),
6519
+ /** Soft-disabled flag. */
6520
+ disabled: z.boolean(),
7055
6521
  parentDeviceId: z.number().nullable(),
6522
+ /** Optional semantic role — `DeviceRole` string. null for top-level devices. */
6523
+ role: z.string().nullable().optional(),
7056
6524
  online: z.boolean(),
6525
+ /** True when the device's initial feature-probe has completed (or it has
6526
+ * no probe) — exported shape is stable; exporters gate advertise on this.
6527
+ * Optional for deploy-order resilience: a record produced by an older
6528
+ * device-manager build omits it, and consumers treat absent as "not ready"
6529
+ * (export gate carries forward, never advertises a partial shape). */
6530
+ probed: z.boolean().optional(),
7057
6531
  features: z.array(z.string()),
7058
- config: z.record(z.string(), z.unknown()),
7059
- /** Optional upstream-system identity (dispatch key + system tag).
7060
- * See `SourceInfo`. Present when the device has a non-synthetic
7061
- * source identifier (HA entities, vendor MAC, …); omitted when the
7062
- * synthetic backfill is in effect. */
7063
- sourceInfo: SourceInfoSchema.optional()
7064
- });
7065
- /**
7066
- * Result of a live field test (e.g. probing an RTSP URL during device
7067
- * creation). Matches the UI-side `FieldProbeResult` in
7068
- * `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
7069
- * returned `labels` as chips next to the input.
7070
- */
7071
- var FieldProbeResultSchema = z.object({
7072
- status: z.enum(["ok", "error"]),
7073
- labels: z.array(z.string()).optional(),
7074
- error: z.string().optional()
7075
- });
7076
- /**
7077
- * The output of `getChildCreationSchema` is a UI schema tree. We store
7078
- * it as `unknown` at the capability layer — the router just passes it
7079
- * through and the admin UI renders it via `FormBuilder`. The actual
7080
- * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
7081
- * but we deliberately avoid a Zod mirror because the union is large and
7082
- * not meant for runtime validation at this seam.
7083
- */
7084
- var CreationSchemaOutputSchema = z.unknown();
7085
- var deviceProviderCapability = {
7086
- name: "device-provider",
7087
- scope: "system",
7088
- mode: "collection",
7089
- methods: {
7090
- start: method(z.void(), z.void(), { kind: "mutation" }),
7091
- stop: method(z.void(), z.void(), { kind: "mutation" }),
7092
- getStatus: method(z.void(), ProviderStatusSchema),
7093
- getDevices: method(z.void(), z.array(z.object({
7094
- id: z.string(),
7095
- name: z.string(),
7096
- type: z.string()
7097
- }))),
7098
- supportsDiscovery: method(z.object({}), z.boolean()),
7099
- /**
7100
- * Run a network scan. `params` carries optional provider-specific scan
7101
- * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
7102
- * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
7103
- * (provider uses its local-network default).
7104
- */
7105
- discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
7106
- kind: "mutation",
7107
- auth: "admin"
7108
- }),
7109
- /**
7110
- * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
7111
- * provider accepts (e.g. Gree's broadcast address for a different subnet).
7112
- * `null` when the provider takes no extra scan params — the generic
7113
- * aggregated scan never renders this; the per-integration scan does.
7114
- */
7115
- getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
7116
- /**
7117
- * The DeviceType this provider creates via manual add (Camera for
7118
- * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
7119
- * provider does not support manual creation. Lets the Add-Device dialog
7120
- * pick the right type instead of assuming Camera.
7121
- */
7122
- getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
7123
- adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
7124
- kind: "mutation",
7125
- auth: "admin"
7126
- }),
7127
- supportsManualCreation: method(z.object({}), z.boolean()),
7128
- /**
7129
- * Fetch the creation form schema for a given DeviceType. Returns
7130
- * `null` when the provider does not support manually creating
7131
- * devices of that type. The output is a `ConfigUISchema` — the
7132
- * router type-asserts it at the boundary.
7133
- */
7134
- getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
7135
- createDevice: method(z.object({
7136
- type: z.enum(DeviceType),
7137
- config: z.record(z.string(), z.unknown())
7138
- }), DeviceSummarySchema, {
7139
- kind: "mutation",
7140
- auth: "admin"
7141
- }),
7142
- /**
7143
- * Test a single field in the creation form before the device has
7144
- * been persisted. Typical use: probing an RTSP URL entered by the
7145
- * user. Providers that don't support field probing return
7146
- * `{ success: true, message: 'Field test not supported' }`.
7147
- *
7148
- * `formValues` is the live snapshot of every field in the form at
7149
- * the moment the user clicked Test — useful for probes that depend
7150
- * on multiple fields together (e.g. Reolink autodetect needs host
7151
- * + credentials + UID + transport mode in a single call). Optional
7152
- * for backwards compatibility; providers free to ignore it.
7153
- */
7154
- testCreationField: method(z.object({
7155
- type: z.enum(DeviceType),
7156
- key: z.string(),
7157
- value: z.unknown(),
7158
- formValues: z.record(z.string(), z.unknown()).optional()
7159
- }), FieldProbeResultSchema, {
7160
- kind: "mutation",
7161
- auth: "admin"
7162
- })
7163
- }
7164
- };
7165
- //#endregion
7166
- //#region src/capabilities/device-manager.cap.ts
7167
- /**
7168
- * Device Manager capability — hub-side singleton that unifies device persistence,
7169
- * live registry access, and all management operations into a single tRPC surface.
7170
- *
7171
- * Replaces:
7172
- * - `device-persistence` capability (persistence methods absorbed here)
7173
- * - `device-management.router.ts` (deleted in Phase 2)
7174
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
7175
- *
7176
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
7177
- * fork into separate processes but never run on remote cluster agents. Therefore:
7178
- * - No nodeId routing needed — this is a pure hub singleton.
7179
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
7180
- * - No shadow registry or cross-node aggregation required.
7181
- *
7182
- * Forked workers register devices back to the hub via `ctx.devices`
7183
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
7184
- */
7185
- /** One child-placement directive on a container's `childLayout`. Structurally
7186
- * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
7187
- * shape for the same field. The child is identified by its re-sync-stable
7188
- * accessory `stableIdSuffix` (`childKey`); listed children are grouped into
7189
- * named accordion sections (with optional intra-section order). */
7190
- var ChildLayoutEntrySchema = z.object({
7191
- childKey: z.string(),
7192
- section: z.string(),
7193
- order: z.number().optional(),
7194
- collapsed: z.boolean().optional()
7195
- });
7196
- /** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
7197
- * `device-management.ts`. Source is a union: a FIELD source copies a sibling
7198
- * accessory's status field (`kind` optional/absent for wire compat); a
7199
- * LITERAL source carries a per-device constant (no sibling is read); a
7200
- * GLOBAL source (P2e) copies ANY device's status field, addressed by the
7201
- * source device's full re-sync-stable `stableId`. */
7202
- var DeviceLinkFieldSourceSchema = z.object({
7203
- kind: z.literal("field").optional(),
7204
- sourceKey: z.string(),
7205
- cap: z.string(),
7206
- fieldPath: z.string()
7207
- });
7208
- var DeviceLinkLiteralSourceSchema = z.object({
7209
- kind: z.literal("literal"),
7210
- value: z.union([
7211
- z.string(),
7212
- z.number(),
7213
- z.boolean(),
7214
- z.null()
7215
- ])
7216
- });
7217
- var DeviceLinkGlobalSourceSchema = z.object({
7218
- kind: z.literal("global"),
7219
- sourceStableId: z.string(),
7220
- cap: z.string(),
7221
- fieldPath: z.string()
7222
- });
7223
- /** Expression source (Stage X): compute the target field from N named bindings
7224
- * via the safe expression engine. Bindings are field | literal | global — never
7225
- * another expression (no nesting). The `superRefine` runs the SAME author-time
7226
- * validation as `validateExpressionSource` (compiles the expr, checks binding
7227
- * names + identifier coverage) so every wire boundary that parses a DeviceLink
7228
- * (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
7229
- * Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
7230
- var DeviceLinkExpressionSourceSchema = z.object({
7231
- kind: z.literal("expression"),
7232
- expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
7233
- bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), z.union([
7234
- DeviceLinkFieldSourceSchema,
7235
- DeviceLinkLiteralSourceSchema,
7236
- DeviceLinkGlobalSourceSchema
7237
- ]))
7238
- }).superRefine((src, ctx) => {
7239
- const err = validateExpressionSource(src);
7240
- if (err !== null) ctx.addIssue({
7241
- code: "custom",
7242
- message: err,
7243
- path: ["expr"]
7244
- });
7245
- });
7246
- var DeviceLinkSchema = z.object({
7247
- id: z.string(),
7248
- source: z.union([
7249
- DeviceLinkFieldSourceSchema,
7250
- DeviceLinkLiteralSourceSchema,
7251
- DeviceLinkGlobalSourceSchema,
7252
- DeviceLinkExpressionSourceSchema
7253
- ]),
7254
- target: z.object({
7255
- cap: z.string(),
7256
- fieldPath: z.string(),
7257
- itemKey: z.string().optional()
7258
- }),
7259
- transform: z.discriminatedUnion("kind", [
7260
- z.object({ kind: z.literal("identity") }),
7261
- z.object({
7262
- kind: z.literal("enum-map"),
7263
- mapping: z.record(z.string(), z.union([
7264
- z.string(),
7265
- z.number(),
7266
- z.boolean()
7267
- ])),
7268
- fallback: z.union([
7269
- z.string(),
7270
- z.number(),
7271
- z.boolean()
7272
- ]).optional()
7273
- }),
7274
- z.object({
7275
- kind: z.literal("linear"),
7276
- scale: z.number(),
7277
- offset: z.number(),
7278
- clamp: z.tuple([z.number(), z.number()]).readonly().optional()
7279
- })
7280
- ]).optional()
7281
- });
7282
- /** Cap-wire shape of a per-cap display refinement — mirrors
7283
- * `DeviceCapDisplayOverride` in `device-management.ts`. */
7284
- var DeviceCapDisplayOverrideSchema = z.object({
7285
- unit: z.string().min(1).optional(),
7286
- precision: z.number().int().min(0).max(10).optional()
7287
- });
7288
- /** Cap-wire shape of an operator-authored per-device display override —
7289
- * mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
7290
- * bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
7291
- var DeviceDisplayOverrideSchema = z.object({
7292
- icon: z.string().min(1).optional(),
7293
- label: z.string().min(1).optional(),
7294
- unit: z.string().min(1).optional(),
7295
- precision: z.number().int().min(0).max(10).optional(),
7296
- hidden: z.boolean().optional(),
7297
- perCap: z.record(z.string(), DeviceCapDisplayOverrideSchema).optional()
7298
- });
7299
- /** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
7300
- * in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
7301
- * the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
7302
- var RoleDisplayDefaultSchema = z.object({
7303
- unit: z.string().min(1).optional(),
7304
- precision: z.number().int().min(0).max(10).optional(),
7305
- icon: z.string().min(1).optional()
7306
- });
7307
- /**
7308
- * Serializable projection of a live IDevice.
7309
- * Returned by listAll, getDevice, getChildren.
7310
- * Live methods (getStreamSources, getConfigSchema) are separate calls.
7311
- */
7312
- var DeviceInfoSchema = z.object({
7313
- /** Progressive, system-wide unique number. Allocated synchronously by
7314
- * `device-manager.allocateDeviceId` BEFORE the owning `IDevice` is
7315
- * constructed, so every live device exposes an `id` — no transient
7316
- * null window. Distinct from `stableId`, which is unique per
7317
- * integration. Ids are monotonic and never reissued on removal. */
7318
- id: z.number(),
7319
- stableId: z.string(),
7320
- addonId: z.string(),
7321
- type: z.enum(DeviceType),
7322
- name: z.string(),
7323
- /** Operator-organisational location label. `null` when unset. */
7324
- location: z.string().nullable(),
7325
- /** Soft-disabled flag. */
7326
- disabled: z.boolean(),
7327
- parentDeviceId: z.number().nullable(),
7328
- /** Optional semantic role — `DeviceRole` string. null for top-level devices. */
7329
- role: z.string().nullable().optional(),
7330
- online: z.boolean(),
7331
- /** True when the device's initial feature-probe has completed (or it has
7332
- * no probe) — exported shape is stable; exporters gate advertise on this.
7333
- * Optional for deploy-order resilience: a record produced by an older
7334
- * device-manager build omits it, and consumers treat absent as "not ready"
7335
- * (export gate carries forward, never advertises a partial shape). */
7336
- probed: z.boolean().optional(),
7337
- features: z.array(z.string()),
7338
- /** true when the device has a getStreamSources() method (ICameraDevice) */
7339
- isCamera: z.boolean(),
7340
- /** Current config values — serializable snapshot */
6532
+ /** true when the device has a getStreamSources() method (ICameraDevice) */
6533
+ isCamera: z.boolean(),
6534
+ /** Current config values serializable snapshot */
7341
6535
  config: z.record(z.string(), z.unknown()),
7342
6536
  /** Hardware + identity blob (manufacturer / model / firmware / sn /
7343
6537
  * uid / mac / …). Populated by drivers; editable via setMetadata. */
@@ -7358,8 +6552,6 @@ var DeviceInfoSchema = z.object({
7358
6552
  * named accordion sections (with optional intra-section order). See
7359
6553
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
7360
6554
  childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
7361
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
7362
- deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
7363
6555
  /** Operator-authored per-device display override. See `DeviceMeta.display`. */
7364
6556
  display: DeviceDisplayOverrideSchema.optional()
7365
6557
  });
@@ -7368,7 +6560,7 @@ var ConfigEntrySchema = z.object({
7368
6560
  value: z.unknown(),
7369
6561
  description: z.string().optional()
7370
6562
  });
7371
- var DeviceLinkModeSchema = z.enum(["auto", "manual"]);
6563
+ var LinkedDevicesModeSchema = z.enum(["auto", "manual"]);
7372
6564
  /** One resolved linked device — the compact projection consumers need. */
7373
6565
  var LinkedDeviceSchema = z.object({
7374
6566
  deviceId: z.number(),
@@ -7431,8 +6623,6 @@ var DeviceMetaSchema = z.object({
7431
6623
  * accordion sections (with optional intra-section order). See
7432
6624
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
7433
6625
  childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
7434
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
7435
- deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
7436
6626
  /** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
7437
6627
  * Optional: only present for accessory children that carry a known role. */
7438
6628
  role: z.string().nullable().optional(),
@@ -7584,16 +6774,6 @@ var deviceManagerCapability = {
7584
6774
  kind: "mutation",
7585
6775
  auth: "admin"
7586
6776
  }),
7587
- /** Set (or replace) the cross-device field wirings on a device's meta row.
7588
- * Mirrors `setChildLayout`; persisted, projected, preserved across
7589
- * re-register/restore. Idempotent. */
7590
- setDeviceLinks: method(z.object({
7591
- deviceId: z.number(),
7592
- deviceLinks: z.array(DeviceLinkSchema).readonly()
7593
- }), z.void(), {
7594
- kind: "mutation",
7595
- auth: "admin"
7596
- }),
7597
6777
  /** Set (or clear) the per-device display override on the meta row. Mirrors
7598
6778
  * `setChildLayout` persistence; `null` clears the override entirely. The
7599
6779
  * override unit(s) are normalized (`normalizeUnit`) at write so the render
@@ -7617,15 +6797,15 @@ var deviceManagerCapability = {
7617
6797
  kind: "mutation",
7618
6798
  auth: "admin"
7619
6799
  }),
7620
- /** List the wireable status-schema fields per cap bound to a device.
7621
- * Powers the Wiring tab's field pickers. Caps without a status schema are
7622
- * omitted. Item-array caps (`status.itemArray`, e.g. consumables) also
7623
- * emit their per-item fields tagged `item: true` (a link targeting one
7624
- * must carry a `target.itemKey`) plus the cap-level `itemArray`
7625
- * descriptor. `includeSynthesizable: true` (TARGET pickers only) unions
7626
- * in unbound device-scoped caps that declare `status.empty` and match
7627
- * the device's type so the FIRST link to a synthesize-only cap
7628
- * (consumables on an HA vacuum) can be authored. */
6800
+ /** The per-cap catalog of readable status-schema fields for one device.
6801
+ * This is the SOURCE PICKER it outlived the Wiring tab that first
6802
+ * needed it (deleted 2026-08-08) and becomes the picker of a composition
6803
+ * recipe: every field the old tab could target, a composed device can
6804
+ * read. Caps without a status schema are omitted. Item-array caps
6805
+ * (`status.itemArray`, e.g. consumables) also emit their per-item fields
6806
+ * tagged `item: true` plus the cap-level `itemArray` descriptor.
6807
+ * `includeSynthesizable: true` unions in unbound device-scoped caps that
6808
+ * declare `status.empty` and match the device's type. */
7629
6809
  getWireableFields: method(z.object({
7630
6810
  deviceId: z.number(),
7631
6811
  includeSynthesizable: z.boolean().optional()
@@ -7767,7 +6947,7 @@ var deviceManagerCapability = {
7767
6947
  * (see the module docblock) — there are no bespoke link mutations.
7768
6948
  */
7769
6949
  getLinkedDevices: method(z.object({ deviceId: z.number() }), z.object({
7770
- mode: DeviceLinkModeSchema,
6950
+ mode: LinkedDevicesModeSchema,
7771
6951
  devices: z.array(LinkedDeviceSchema)
7772
6952
  })),
7773
6953
  /** Get stream sources for a camera device. */
@@ -7831,11 +7011,7 @@ var deviceManagerCapability = {
7831
7011
  deviceId: z.number(),
7832
7012
  entries: z.array(z.object({
7833
7013
  capName: z.string(),
7834
- kind: z.enum([
7835
- "native",
7836
- "wrapped",
7837
- "linked"
7838
- ]),
7014
+ kind: z.enum(["native", "wrapped"]),
7839
7015
  providerAddonId: z.string(),
7840
7016
  providerNodeId: z.string(),
7841
7017
  nativeAddonId: z.string()
@@ -7853,11 +7029,7 @@ var deviceManagerCapability = {
7853
7029
  deviceId: z.number(),
7854
7030
  entries: z.array(z.object({
7855
7031
  capName: z.string(),
7856
- kind: z.enum([
7857
- "native",
7858
- "wrapped",
7859
- "linked"
7860
- ]),
7032
+ kind: z.enum(["native", "wrapped"]),
7861
7033
  providerAddonId: z.string(),
7862
7034
  providerNodeId: z.string(),
7863
7035
  nativeAddonId: z.string()
@@ -9103,6 +8275,10 @@ var motionDetectionCapability = {
9103
8275
  mode: "singleton",
9104
8276
  kind: "wrapper",
9105
8277
  defaultActive: true,
8278
+ /** Frame-differencing motion analysis over a video stream — only a camera
8279
+ * produces the frames. Read by the `defaultActive` auto-bind in
8280
+ * `device-manager.getBindings` to decide which devices it may claim. */
8281
+ deviceTypes: [DeviceType.Camera],
9106
8282
  exposesDeviceSettings: true,
9107
8283
  methods: {
9108
8284
  analyze: method(z.object({
@@ -12224,6 +11400,18 @@ var OauthIntegrationDescriptorSchema = z.object({
12224
11400
  * redirect_uri that does not start with one of these. Required —
12225
11401
  * an empty list means the integration can never complete linking. */
12226
11402
  allowedRedirectPrefixes: z.array(z.string()).min(1),
11403
+ /** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
11404
+ * RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
11405
+ * `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
11406
+ * whose address the hub cannot know in advance (a Home Assistant at
11407
+ * `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
11408
+ * exactly; a public host never satisfies this branch, so it is not a
11409
+ * wildcard prefix by another name. */
11410
+ allowedPrivateHostPaths: z.array(z.string()).optional(),
11411
+ /** When true this is a PUBLIC client (source is published, no secret can be
11412
+ * protected) and PKCE is mandatory: `/authorize` refuses without an S256
11413
+ * `code_challenge`, `/token` refuses without the matching `code_verifier`. */
11414
+ requiresPkce: z.boolean().optional(),
12227
11415
  /** Optional public origin (no trailing slash) that this integration's
12228
11416
  * issued codes/tokens should carry as the `hubUrl` claim — typically the
12229
11417
  * operator-selected external-access endpoint resolved by the addon. When
@@ -12382,7 +11570,7 @@ var TrackEnvelopeSchema = z.object({
12382
11570
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
12383
11571
  * keeps every scalar the list surfaces actually render (ids, class(es),
12384
11572
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
12385
- * zonesVisited, bestEventId, envelope) and returns `positions` /
11573
+ * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
12386
11574
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
12387
11575
  * `getTrack`. Mirrors the event-store `projection` convention
12388
11576
  * (`getObjectEvents` et al.).
@@ -12635,6 +11823,24 @@ var TrackSchema = z.object({
12635
11823
  * Populated from the persisted envelope columns on historical reads;
12636
11824
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
12637
11825
  envelope: TrackEnvelopeSchema.optional(),
11826
+ /**
11827
+ * A face DETECTOR found a face on this track — nothing more. It says the
11828
+ * detail plane produced a `face` detail; it does NOT say the face was
11829
+ * embedded, matched, above `minFacePx`, or that the recognizer was even
11830
+ * enabled. Set once and never cleared.
11831
+ *
11832
+ * **This exists so "face present but not recognised" is expressible.** A
11833
+ * recognised identity lands in `subLabel` (attributed to the face chain via
11834
+ * `subLabelMeta.stepId`), so before this field a track with an unmatched face
11835
+ * and a track with no face at all were byte-identical on the wire and no
11836
+ * surface could tell them apart. The read is `hasFace === true && subLabel
11837
+ * === undefined`.
11838
+ *
11839
+ * **Absent ≠ false.** Every row written before the column existed omits it,
11840
+ * and so does every server that predates the field — a consumer must test
11841
+ * `=== true` and render nothing otherwise, never infer "no face".
11842
+ */
11843
+ hasFace: z.boolean().optional(),
12638
11844
  ...TrackFlagFields,
12639
11845
  ...TrackRetrainFields
12640
11846
  });
@@ -16576,6 +15782,10 @@ var SsoBridgeClaimsSchema = z.object({
16576
15782
  integrationId: z.string().optional(),
16577
15783
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
16578
15784
  jti: z.string().optional(),
15785
+ /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
15786
+ * client. Its PRESENCE is what makes the verifier mandatory at exchange,
15787
+ * so the requirement travels with the code and not with mutable config. */
15788
+ codeChallenge: z.string().optional(),
16579
15789
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
16580
15790
  * tokens so the verify path can check the session is not revoked. */
16581
15791
  sessionId: z.string().optional()
@@ -17348,6 +16558,10 @@ var videoclipsCapability = {
17348
16558
  mode: "singleton",
17349
16559
  kind: "wrapper",
17350
16560
  defaultActive: true,
16561
+ /** A clip is a window over a camera's footage — the cap is meaningless on a
16562
+ * sensor, a button or an event emitter, and the `defaultActive` auto-bind
16563
+ * reads this to decide which devices it may claim. */
16564
+ deviceTypes: [DeviceType.Camera],
17351
16565
  methods: {
17352
16566
  listClips: method(z.object({
17353
16567
  deviceId: z.number(),
@@ -19899,7 +19113,29 @@ var FaceInfoSchema = z.object({
19899
19113
  recognizedIdentityId: z.string().optional(),
19900
19114
  identityName: z.string().optional(),
19901
19115
  assigned: z.boolean(),
19116
+ /**
19117
+ * The crop, inline, base64.
19118
+ *
19119
+ * **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
19120
+ * field alone is ~2.87 MiB, re-sent in full on every operator assign and
19121
+ * every 30 s poll, base64-inflated over the msgpack socket and held in the
19122
+ * query heap. It stays for callers that have not migrated; `includeCrops:
19123
+ * false` turns it off once they have.
19124
+ */
19902
19125
  base64: z.string().optional(),
19126
+ /**
19127
+ * Same crop, as a data-plane URL for `<img src>` — the move the admin
19128
+ * snapshot surfaces made on 2026-08-08.
19129
+ *
19130
+ * Served by the `event-media` plane, which resolves a raw MediaStore key and
19131
+ * is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
19132
+ * cookie, so no header plumbing is needed. The bytes then ride the browser's
19133
+ * HTTP cache with an ETag and `immutable`, instead of the WebSocket.
19134
+ *
19135
+ * Absent when the face has no stored crop, or when the addon has no data
19136
+ * plane — callers fall back to {@link base64}.
19137
+ */
19138
+ cropUrl: z.string().optional(),
19903
19139
  /** Design B: the face bbox (pixel space) on the key frame — lets a detail
19904
19140
  * view draw the box over the native `keyFrameMediaKey` frame. Absent on
19905
19141
  * legacy rows written before design B. */
@@ -19975,7 +19211,23 @@ var faceGalleryCapability = {
19975
19211
  }),
19976
19212
  listRecentFaces: method(z.object({
19977
19213
  limit: z.number().int().positive().optional(),
19978
- filter: FaceFilterEnum.optional()
19214
+ filter: FaceFilterEnum.optional(),
19215
+ /**
19216
+ * Inline the base64 crop on every row. Default `true` — the existing
19217
+ * behaviour, kept so no caller breaks.
19218
+ *
19219
+ * Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
19220
+ * drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
19221
+ * the browser cache the images.
19222
+ *
19223
+ * **This is an INPUT field, so it does not reach the addon until the
19224
+ * next train.** The hub router validates cap inputs against its own
19225
+ * compiled Zod, which strips a key it does not know — verified today
19226
+ * on the OUTPUT side, where an additive field DOES arrive immediately
19227
+ * (`Track.hasFace`). Until the train ships, sending `false` is
19228
+ * harmless and simply keeps the crops inline.
19229
+ */
19230
+ includeCrops: z.boolean().optional()
19979
19231
  }).optional(), z.array(FaceInfoSchema).readonly()),
19980
19232
  getFaceByTrack: method(z.object({
19981
19233
  deviceId: z.number().int(),
@@ -25488,14 +24740,19 @@ var userManagementCapability = {
25488
24740
  username: z.string(),
25489
24741
  scopes: z.array(TokenScopeSchema),
25490
24742
  redirectUri: z.string(),
25491
- hubUrl: z.string()
24743
+ hubUrl: z.string(),
24744
+ /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
24745
+ * that carries one can ONLY be exchanged with the matching verifier. */
24746
+ codeChallenge: z.string().optional()
25492
24747
  }), z.object({ code: z.string() }), {
25493
24748
  kind: "mutation",
25494
24749
  access: "create"
25495
24750
  }),
25496
24751
  oauthExchangeCode: method(z.object({
25497
24752
  code: z.string(),
25498
- redirectUri: z.string()
24753
+ redirectUri: z.string(),
24754
+ /** PKCE verifier. REQUIRED when the code carries a challenge. */
24755
+ codeVerifier: z.string().optional()
25499
24756
  }), z.object({
25500
24757
  accessToken: z.string(),
25501
24758
  refreshToken: z.string(),
@@ -27386,6 +26643,218 @@ var BaseDeviceProvider = class extends BaseAddon {
27386
26643
  }
27387
26644
  };
27388
26645
  //#endregion
26646
+ //#region src/device/declared-device.ts
26647
+ /** Marker written to a declared integration's `info`. */
26648
+ var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
26649
+ /**
26650
+ * Strip the `<node>/<addon>` suffix a forked child carries.
26651
+ *
26652
+ * Comparing `ctx.kernel.localNodeId` raw skipped EVERY node — including the one
26653
+ * that was supposed to act — because on the hub it reads `hub/<addon>`.
26654
+ */
26655
+ function declarationOwnerNodeId(localNodeId) {
26656
+ const raw = localNodeId ?? "hub";
26657
+ if (!raw.includes("/")) return raw;
26658
+ return raw.split("/")[0] ?? "hub";
26659
+ }
26660
+ /** Cap on how many rows one withdrawal pass may delete. */
26661
+ var DECLARED_DEVICE_SWEEP_LIMIT = 32;
26662
+ /**
26663
+ * The one way an addon owns a device it declares.
26664
+ *
26665
+ * Construct once with the addon's ports, then call {@link reconcile} on boot and
26666
+ * on every convergence tick. There is no second get-or-create helper — a guard
26667
+ * in `scripts/` enforces that.
26668
+ */
26669
+ var DeclaredDevices = class {
26670
+ ports;
26671
+ constructor(ports) {
26672
+ this.ports = ports;
26673
+ }
26674
+ /**
26675
+ * Converge the declared set. Idempotent, and safe to call repeatedly.
26676
+ *
26677
+ * Throws only what the ports throw on the FIRST index read; every other
26678
+ * failure is per-device and logged, so one bad declaration never takes the
26679
+ * others down.
26680
+ */
26681
+ async reconcile(spec) {
26682
+ if ((spec.placement ?? "hub") === "hub") {
26683
+ const nodeId = declarationOwnerNodeId(this.ports.localNodeId);
26684
+ if (nodeId !== "hub") {
26685
+ this.ports.logger.info("declared devices are hub-owned — skipping on this node", { meta: {
26686
+ nodeId,
26687
+ rawNodeId: this.ports.localNodeId ?? null
26688
+ } });
26689
+ return {
26690
+ integrationId: null,
26691
+ devices: [],
26692
+ removed: [],
26693
+ owned: false
26694
+ };
26695
+ }
26696
+ }
26697
+ const integrationId = await this.ensureIntegration(spec.integrationName);
26698
+ const index = await this.readIndex();
26699
+ const outcomes = [];
26700
+ for (const declaration of spec.devices) {
26701
+ const outcome = await this.applyDeclaration(declaration, integrationId, index);
26702
+ if (outcome !== null) outcomes.push(outcome);
26703
+ }
26704
+ return {
26705
+ integrationId,
26706
+ devices: outcomes,
26707
+ removed: await this.sweepWithdrawn(spec.devices, integrationId, index),
26708
+ owned: true
26709
+ };
26710
+ }
26711
+ /**
26712
+ * Get-or-create the FIXED integration, and RE-ASSERT the flag every pass.
26713
+ *
26714
+ * The re-assertion is the fix for the defect the hand-rolled version shipped
26715
+ * with: writing `info.fixed` only on the create path left every pre-existing
26716
+ * install without it, and the kernel kept offering to delete an integration
26717
+ * the addon owns.
26718
+ */
26719
+ async ensureIntegration(integrationName) {
26720
+ const existing = await this.ports.getIntegration(this.ports.addonId);
26721
+ if (existing === null) {
26722
+ const created = await this.ports.createIntegration({
26723
+ addonId: this.ports.addonId,
26724
+ name: integrationName,
26725
+ info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
26726
+ });
26727
+ this.ports.logger.info("declared a fixed integration", { meta: {
26728
+ integrationId: created.id,
26729
+ name: integrationName
26730
+ } });
26731
+ return created.id;
26732
+ }
26733
+ if (existing.info?.["fixed"] !== true) {
26734
+ await this.ports.updateIntegration({
26735
+ id: existing.id,
26736
+ info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
26737
+ });
26738
+ this.ports.logger.info("re-asserted `fixed` on a declared integration", { meta: { integrationId: existing.id } });
26739
+ }
26740
+ return existing.id;
26741
+ }
26742
+ async readIndex() {
26743
+ const rows = await this.ports.listOwnDevices();
26744
+ return new Map(rows.map((row) => [row.stableId, row]));
26745
+ }
26746
+ /**
26747
+ * One declaration: adopt what exists, create what does not.
26748
+ *
26749
+ * The create branch is the destructive one — it seeds `initialMeta`, and
26750
+ * `initialMeta.name` lands as an unconditional `setName`. A transiently empty
26751
+ * index therefore looks exactly like a first boot and would silently re-stamp
26752
+ * the declared name over the operator's rename. D49: that branch needs a
26753
+ * second read to agree.
26754
+ */
26755
+ async applyDeclaration(declaration, integrationId, index) {
26756
+ try {
26757
+ let existing = index.get(declaration.stableId);
26758
+ if (existing === void 0) {
26759
+ existing = (await this.readIndex()).get(declaration.stableId);
26760
+ if (existing !== void 0) this.ports.logger.warn("device index disagreed with itself — adopting instead of re-creating", {
26761
+ tags: { deviceId: existing.id },
26762
+ meta: {
26763
+ stableId: declaration.stableId,
26764
+ addonId: this.ports.addonId
26765
+ }
26766
+ });
26767
+ }
26768
+ if (existing !== void 0) {
26769
+ const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, {}, null, void 0);
26770
+ this.ports.logger.info("declared device adopted", {
26771
+ tags: { deviceId: device.id },
26772
+ meta: {
26773
+ stableId: declaration.stableId,
26774
+ integrationId
26775
+ }
26776
+ });
26777
+ return {
26778
+ stableId: declaration.stableId,
26779
+ deviceId: device.id,
26780
+ device,
26781
+ created: false
26782
+ };
26783
+ }
26784
+ const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, declaration.config ?? {}, null, {
26785
+ type: declaration.type,
26786
+ name: declaration.name,
26787
+ integrationId,
26788
+ ...declaration.role === void 0 ? {} : { role: declaration.role }
26789
+ });
26790
+ this.ports.logger.info("declared device created", {
26791
+ tags: { deviceId: device.id },
26792
+ meta: {
26793
+ stableId: declaration.stableId,
26794
+ integrationId
26795
+ }
26796
+ });
26797
+ return {
26798
+ stableId: declaration.stableId,
26799
+ deviceId: device.id,
26800
+ device,
26801
+ created: true
26802
+ };
26803
+ } catch (err) {
26804
+ this.ports.logger.warn("a declared device could not be brought up", { meta: {
26805
+ stableId: declaration.stableId,
26806
+ error: err instanceof Error ? err.message : String(err)
26807
+ } });
26808
+ return null;
26809
+ }
26810
+ }
26811
+ /**
26812
+ * Remove rows under the addon's FIXED integration whose declaration is gone.
26813
+ *
26814
+ * Bounded to that integration: a declared integration has no operator
26815
+ * add-flow, so every row under it got there by declaration. Devices this
26816
+ * addon owns OUTSIDE it (a provider's adopted devices) are never candidates.
26817
+ *
26818
+ * Bounded in count, and every deletion is logged with its `deviceId` — a
26819
+ * withdrawal that removes an operator-visible row silently is the failure
26820
+ * mode, not the removal itself.
26821
+ */
26822
+ async sweepWithdrawn(declarations, integrationId, index) {
26823
+ const declared = new Set(declarations.map((d) => d.stableId));
26824
+ const candidates = [...index.values()].filter((row) => row.integrationId === integrationId && !declared.has(row.stableId));
26825
+ if (candidates.length === 0) return [];
26826
+ if (candidates.length > 32) {
26827
+ this.ports.logger.warn("withdrawal sweep exceeded its bound — removing nothing", { meta: {
26828
+ integrationId,
26829
+ candidates: candidates.length,
26830
+ bound: 32
26831
+ } });
26832
+ return [];
26833
+ }
26834
+ const removed = [];
26835
+ for (const row of candidates) try {
26836
+ await this.ports.devices.remove(row.id);
26837
+ removed.push(row.id);
26838
+ this.ports.logger.info("declared device removed — its declaration was withdrawn", {
26839
+ tags: { deviceId: row.id },
26840
+ meta: {
26841
+ stableId: row.stableId,
26842
+ integrationId
26843
+ }
26844
+ });
26845
+ } catch (err) {
26846
+ this.ports.logger.warn("a withdrawn declared device could not be removed", {
26847
+ tags: { deviceId: row.id },
26848
+ meta: {
26849
+ stableId: row.stableId,
26850
+ error: err instanceof Error ? err.message : String(err)
26851
+ }
26852
+ });
26853
+ }
26854
+ return removed;
26855
+ }
26856
+ };
26857
+ //#endregion
27389
26858
  //#region src/device/device-control-resolution.ts
27390
26859
  /**
27391
26860
  * Device-control RESOLUTION LOGIC — the single, presentation-free source of
@@ -27562,30 +27031,6 @@ function resolveMutate(router, method) {
27562
27031
  return typeof mutate === "function" ? mutate : null;
27563
27032
  }
27564
27033
  //#endregion
27565
- //#region src/device/device-link-transform.ts
27566
- /** Apply a link transform to a resolved source value. `identity`/undefined
27567
- * pass through; `enum-map` remaps strings (fallback or null when unknown);
27568
- * `linear` does scale*x+offset with optional clamp (null for non-numbers). */
27569
- function applyTransform(value, transform) {
27570
- if (!transform || transform.kind === "identity") return value;
27571
- if (transform.kind === "enum-map") {
27572
- const key = String(value);
27573
- if (Object.prototype.hasOwnProperty.call(transform.mapping, key)) return transform.mapping[key];
27574
- return transform.fallback ?? null;
27575
- }
27576
- if (transform.kind === "linear") {
27577
- if (typeof value !== "number" || Number.isNaN(value)) return null;
27578
- let out = value * transform.scale + transform.offset;
27579
- if (transform.clamp) {
27580
- const min = transform.clamp[0];
27581
- const max = transform.clamp[1];
27582
- out = Math.min(max, Math.max(min, out));
27583
- }
27584
- return out;
27585
- }
27586
- return value;
27587
- }
27588
- //#endregion
27589
27034
  //#region src/device/device-profile.ts
27590
27035
  /**
27591
27036
  * Heuristic check — `device.features?.includes(profile.when.hasFeature)`.
@@ -28114,501 +27559,1347 @@ var SystemMirror = class {
28114
27559
  const proxy = this.getDeviceById(deviceId);
28115
27560
  if (proxy) out.push(proxy);
28116
27561
  }
28117
- return out;
28118
- }
28119
- /** Map every device's slice for `capName` to a derived value. Devices
28120
- * without the cap or without a slice are skipped. */
28121
- mapState(capName, mapper) {
28122
- const out = [];
28123
- for (const [deviceId, perCap] of this.stateMirror) {
28124
- const slice = perCap.get(capName);
28125
- if (!slice) continue;
28126
- out.push(mapper(slice, deviceId));
27562
+ return out;
27563
+ }
27564
+ /** Map every device's slice for `capName` to a derived value. Devices
27565
+ * without the cap or without a slice are skipped. */
27566
+ mapState(capName, mapper) {
27567
+ const out = [];
27568
+ for (const [deviceId, perCap] of this.stateMirror) {
27569
+ const slice = perCap.get(capName);
27570
+ if (!slice) continue;
27571
+ out.push(mapper(slice, deviceId));
27572
+ }
27573
+ return out;
27574
+ }
27575
+ /** First device whose slice matches `predicate`, or null. */
27576
+ findState(capName, predicate) {
27577
+ for (const [deviceId, perCap] of this.stateMirror) {
27578
+ const slice = perCap.get(capName);
27579
+ if (!slice) continue;
27580
+ if (!predicate(slice, deviceId)) continue;
27581
+ return this.getDeviceById(deviceId);
27582
+ }
27583
+ return null;
27584
+ }
27585
+ /** Count devices that bind a cap. Faster than `filterByCap(...).length`. */
27586
+ countByCap(capName) {
27587
+ let n = 0;
27588
+ for (const binding of this.bindings.values()) if (binding.entries.some((e) => e.capName === capName)) n++;
27589
+ return n;
27590
+ }
27591
+ /** Count devices whose slice for `capName` matches `predicate`. */
27592
+ countByState(capName, predicate) {
27593
+ let n = 0;
27594
+ for (const [deviceId, perCap] of this.stateMirror) {
27595
+ const slice = perCap.get(capName);
27596
+ if (!slice) continue;
27597
+ if (predicate(slice, deviceId)) n++;
27598
+ }
27599
+ return n;
27600
+ }
27601
+ /**
27602
+ * Global listener — fires for every `device.state-changed` event the
27603
+ * mirror absorbs.
27604
+ */
27605
+ listen(cb) {
27606
+ this.globalStateListeners.add(cb);
27607
+ return () => {
27608
+ this.globalStateListeners.delete(cb);
27609
+ };
27610
+ }
27611
+ /**
27612
+ * Per-cap listener — fires only for state changes on `capName`,
27613
+ * across every device. The callback receives the deviceId so the
27614
+ * caller can route.
27615
+ */
27616
+ listenCap(capName, cb) {
27617
+ let set = this.capListeners.get(capName);
27618
+ if (!set) {
27619
+ set = /* @__PURE__ */ new Set();
27620
+ this.capListeners.set(capName, set);
27621
+ }
27622
+ set.add(cb);
27623
+ return () => {
27624
+ set.delete(cb);
27625
+ if (set.size === 0) this.capListeners.delete(capName);
27626
+ };
27627
+ }
27628
+ /**
27629
+ * Per-device listener — fires for every cap change on `deviceId`.
27630
+ */
27631
+ listenDevice(deviceId, cb) {
27632
+ let set = this.deviceListeners.get(deviceId);
27633
+ if (!set) {
27634
+ set = /* @__PURE__ */ new Set();
27635
+ this.deviceListeners.set(deviceId, set);
27636
+ }
27637
+ set.add(cb);
27638
+ return () => {
27639
+ set.delete(cb);
27640
+ if (set.size === 0) this.deviceListeners.delete(deviceId);
27641
+ };
27642
+ }
27643
+ /** Fires when `device.registered` lands. Receives the new metadata. */
27644
+ onDeviceAdded(cb) {
27645
+ this.addedListeners.add(cb);
27646
+ return () => {
27647
+ this.addedListeners.delete(cb);
27648
+ };
27649
+ }
27650
+ /** Fires when `device.unregistered` lands. `info` is the LAST-known
27651
+ * metadata (or null if the device was never seen). */
27652
+ onDeviceRemoved(cb) {
27653
+ this.removedListeners.add(cb);
27654
+ return () => {
27655
+ this.removedListeners.delete(cb);
27656
+ };
27657
+ }
27658
+ /**
27659
+ * Resolve when `predicate` over the runtime-state slice for
27660
+ * `(deviceId, capName)` becomes true. Resolves immediately if the
27661
+ * current slice already matches. Rejects with `Error('timeout')`
27662
+ * after `timeoutMs` (default 30s; pass `Infinity` to wait forever).
27663
+ *
27664
+ * Returns the matching slice — caller can read it directly without
27665
+ * a second mirror lookup.
27666
+ */
27667
+ waitForState(deviceId, capName, predicate, timeoutMs = 3e4) {
27668
+ return new Promise((resolve, reject) => {
27669
+ const check = () => {
27670
+ const slice = this.stateMirror.get(deviceId)?.get(capName);
27671
+ if (slice && predicate(slice)) return slice;
27672
+ return null;
27673
+ };
27674
+ const initial = check();
27675
+ if (initial) {
27676
+ resolve(initial);
27677
+ return;
27678
+ }
27679
+ let timer = null;
27680
+ const off = this.listenDevice(deviceId, (_id, cap, slice) => {
27681
+ if (cap !== capName) return;
27682
+ if (!slice) return;
27683
+ if (predicate(slice)) {
27684
+ if (timer) clearTimeout(timer);
27685
+ off();
27686
+ resolve(slice);
27687
+ }
27688
+ });
27689
+ if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
27690
+ off();
27691
+ reject(/* @__PURE__ */ new Error(`waitForState timed out after ${timeoutMs}ms (deviceId=${deviceId}, capName=${capName})`));
27692
+ }, timeoutMs);
27693
+ });
27694
+ }
27695
+ /**
27696
+ * Resolve when a device with `deviceId` becomes available (a
27697
+ * binding exists). Resolves immediately if already known. Rejects
27698
+ * with timeout.
27699
+ */
27700
+ waitForDevice(deviceId, timeoutMs = 3e4) {
27701
+ return new Promise((resolve, reject) => {
27702
+ const existing = this.getDeviceById(deviceId);
27703
+ if (existing) {
27704
+ resolve(existing);
27705
+ return;
27706
+ }
27707
+ let timer = null;
27708
+ const off = this.onDeviceAdded((id) => {
27709
+ if (id !== deviceId) return;
27710
+ const proxy = this.getDeviceById(id);
27711
+ if (!proxy) return;
27712
+ if (timer) clearTimeout(timer);
27713
+ off();
27714
+ resolve(proxy);
27715
+ });
27716
+ if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
27717
+ off();
27718
+ reject(/* @__PURE__ */ new Error(`waitForDevice timed out after ${timeoutMs}ms (deviceId=${deviceId})`));
27719
+ }, timeoutMs);
27720
+ });
27721
+ }
27722
+ /**
27723
+ * Iterate every device that binds `capName`. Awaits each callback
27724
+ * sequentially — for parallel use `invokeCap` with explicit
27725
+ * parallelism.
27726
+ */
27727
+ async forEachCap(capName, cb) {
27728
+ for (const proxy of this.filterByCap(capName)) await cb(proxy);
27729
+ }
27730
+ /**
27731
+ * Invoke a cap method on every device that binds the cap. Returns
27732
+ * one result per device, success or failure isolated. Optional
27733
+ * parallelism cap — useful for "snapshot all cameras but only 4
27734
+ * at a time so battery cams don't all wake at once".
27735
+ *
27736
+ * Example:
27737
+ *
27738
+ * const results = await sm.invokeCap('snapshot', 'getSnapshot', {}, { parallelism: 4 })
27739
+ * const failed = results.filter(r => !r.ok)
27740
+ */
27741
+ async invokeCap(capName, methodName, args, opts = {}) {
27742
+ const targets = this.filterByCap(capName);
27743
+ const parallelism = Math.max(1, opts.parallelism ?? targets.length);
27744
+ const out = [];
27745
+ for (let i = 0; i < targets.length; i += parallelism) {
27746
+ const chunk = targets.slice(i, i + parallelism);
27747
+ const settled = await Promise.allSettled(chunk.map(async (proxy) => {
27748
+ const cap = proxy[capName];
27749
+ if (!cap || typeof cap[methodName] !== "function") throw new Error(`device '${proxy.deviceId}' does not expose '${capName}.${methodName}'`);
27750
+ return await cap[methodName](args);
27751
+ }));
27752
+ for (let j = 0; j < settled.length; j++) {
27753
+ const proxy = chunk[j];
27754
+ const r = settled[j];
27755
+ if (r.status === "fulfilled") out.push({
27756
+ deviceId: proxy.deviceId,
27757
+ ok: true,
27758
+ result: r.value
27759
+ });
27760
+ else out.push({
27761
+ deviceId: proxy.deviceId,
27762
+ ok: false,
27763
+ error: r.reason
27764
+ });
27765
+ }
27766
+ }
27767
+ return out;
27768
+ }
27769
+ /**
27770
+ * One-shot summary — fleet size, breakdown by cap / addon / type.
27771
+ * Designed for REPL inspection (`sm.summary()`).
27772
+ */
27773
+ summary() {
27774
+ const byCap = {};
27775
+ const byAddon = {};
27776
+ const byType = {};
27777
+ let online = 0;
27778
+ let offline = 0;
27779
+ for (const binding of this.bindings.values()) for (const entry of binding.entries) byCap[entry.capName] = (byCap[entry.capName] ?? 0) + 1;
27780
+ for (const info of this.devices.values()) {
27781
+ byAddon[info.addonId] = (byAddon[info.addonId] ?? 0) + 1;
27782
+ byType[info.type] = (byType[info.type] ?? 0) + 1;
27783
+ if (info.online) online++;
27784
+ else offline++;
27785
+ }
27786
+ return {
27787
+ totalDevices: this.bindings.size,
27788
+ online,
27789
+ offline,
27790
+ byCap,
27791
+ byAddon,
27792
+ byType,
27793
+ statedDevices: this.stateMirror.size
27794
+ };
27795
+ }
27796
+ /**
27797
+ * Debug-friendly dump — full state + binding + metadata for one
27798
+ * device or all devices. Cheap deep clone so caller mutations don't
27799
+ * leak into the mirror.
27800
+ */
27801
+ dump(deviceId) {
27802
+ const dumpOne = (id) => {
27803
+ const info = this.devices.get(id) ?? null;
27804
+ const binding = this.bindings.get(id) ?? null;
27805
+ const state = {};
27806
+ const perCap = this.stateMirror.get(id);
27807
+ if (perCap) for (const [cap, slice] of perCap) state[cap] = { ...slice };
27808
+ return {
27809
+ deviceId: id,
27810
+ info,
27811
+ binding: binding ? {
27812
+ ...binding,
27813
+ entries: binding.entries.map((e) => ({ ...e }))
27814
+ } : null,
27815
+ state
27816
+ };
27817
+ };
27818
+ if (deviceId !== void 0) return dumpOne(deviceId);
27819
+ const out = [];
27820
+ for (const id of this.bindings.keys()) out.push(dumpOne(id));
27821
+ return out;
27822
+ }
27823
+ /**
27824
+ * Direct read-only access to the underlying state mirror. Use
27825
+ * sparingly — `getSystemState` returns a deep copy that's safer for
27826
+ * exploratory work; this avoids the clone cost when iterating
27827
+ * thousands of slices.
27828
+ */
27829
+ getRawMirror() {
27830
+ return this.stateMirror;
27831
+ }
27832
+ /**
27833
+ * Snapshot of the full state mirror — same shape as the warm-boot
27834
+ * payload. Deep-cloned; safe to mutate.
27835
+ */
27836
+ getSystemState() {
27837
+ const copy = /* @__PURE__ */ new Map();
27838
+ for (const [id, perCap] of this.stateMirror) {
27839
+ const dup = /* @__PURE__ */ new Map();
27840
+ for (const [k, v] of perCap) dup.set(k, { ...v });
27841
+ copy.set(id, dup);
27842
+ }
27843
+ return copy;
27844
+ }
27845
+ subscribeBus() {
27846
+ if (!this.api.live?.onEvent) return;
27847
+ const sub = this.api.live.onEvent;
27848
+ this.bridges.push(sub.subscribe({ category: STATE_CHANGED_CATEGORY }, { onData: (evt) => {
27849
+ const data = evt.data;
27850
+ if (!data || typeof data.deviceId !== "number" || typeof data.capName !== "string") return;
27851
+ this.applyStateUpdate(data.deviceId, data.capName, data.slice);
27852
+ } }));
27853
+ this.bridges.push(sub.subscribe({ category: BINDING_CHANGED_CATEGORY }, { onData: (evt) => {
27854
+ const data = evt.data;
27855
+ const deviceId = typeof data?.deviceId === "number" ? data.deviceId : data?.source?.type === "device" && typeof data.source.id === "number" ? data.source.id : null;
27856
+ if (deviceId === null) return;
27857
+ this.refreshBinding(deviceId);
27858
+ } }));
27859
+ this.bridges.push(sub.subscribe({ category: DEVICE_REGISTERED_CATEGORY }, { onData: (evt) => {
27860
+ const data = evt.data;
27861
+ if (typeof data?.deviceId !== "number") return;
27862
+ this.refreshDeviceMetadata(data.deviceId, "added");
27863
+ } }));
27864
+ this.bridges.push(sub.subscribe({ category: DEVICE_UNREGISTERED_CATEGORY }, { onData: (evt) => {
27865
+ const data = evt.data;
27866
+ if (typeof data?.deviceId !== "number") return;
27867
+ this.applyDeviceRemoval(data.deviceId);
27868
+ } }));
27869
+ this.bridges.push(sub.subscribe({ category: DEVICE_UPDATED_CATEGORY }, { onData: (evt) => {
27870
+ const data = evt.data;
27871
+ if (typeof data?.deviceId !== "number") return;
27872
+ this.refreshDeviceMetadata(data.deviceId, "updated");
27873
+ } }));
27874
+ }
27875
+ applyStateUpdate(deviceId, capName, slice) {
27876
+ let perCap = this.stateMirror.get(deviceId);
27877
+ if (!perCap) {
27878
+ perCap = /* @__PURE__ */ new Map();
27879
+ this.stateMirror.set(deviceId, perCap);
27880
+ }
27881
+ if (slice === void 0) perCap.delete(capName);
27882
+ else perCap.set(capName, slice);
27883
+ const handleKey = `${deviceId}:${capName}`;
27884
+ const handleSet = this.handleListeners.get(handleKey);
27885
+ if (handleSet) for (const cb of handleSet) try {
27886
+ cb(slice);
27887
+ } catch {}
27888
+ for (const cb of this.globalStateListeners) try {
27889
+ cb(deviceId, capName, slice);
27890
+ } catch {}
27891
+ const capSet = this.capListeners.get(capName);
27892
+ if (capSet) for (const cb of capSet) try {
27893
+ cb(deviceId, slice);
27894
+ } catch {}
27895
+ const devSet = this.deviceListeners.get(deviceId);
27896
+ if (devSet) for (const cb of devSet) try {
27897
+ cb(deviceId, capName, slice);
27898
+ } catch {}
27899
+ }
27900
+ applyDeviceRemoval(deviceId) {
27901
+ const lastInfo = this.devices.get(deviceId) ?? null;
27902
+ this.bindings.delete(deviceId);
27903
+ this.devices.delete(deviceId);
27904
+ this.stateMirror.delete(deviceId);
27905
+ for (const cb of this.removedListeners) try {
27906
+ cb(deviceId, lastInfo);
27907
+ } catch {}
27908
+ }
27909
+ async refreshBinding(deviceId) {
27910
+ try {
27911
+ const fresh = (await this.api.deviceManager.getAllBindings.query({})).find((b) => b.deviceId === deviceId);
27912
+ if (fresh) this.bindings.set(deviceId, fresh);
27913
+ else this.applyDeviceRemoval(deviceId);
27914
+ } catch {}
27915
+ }
27916
+ async refreshDeviceMetadata(deviceId, kind) {
27917
+ try {
27918
+ const info = await this.api.deviceManager.getDevice.query({ deviceId });
27919
+ if (!info) return;
27920
+ const wasNew = !this.devices.has(deviceId);
27921
+ this.devices.set(deviceId, info);
27922
+ if (kind === "added" && wasNew) {
27923
+ await this.refreshBinding(deviceId);
27924
+ for (const cb of this.addedListeners) try {
27925
+ cb(deviceId, info);
27926
+ } catch {}
27927
+ }
27928
+ } catch {}
27929
+ }
27930
+ };
27931
+ function inSet(value, set) {
27932
+ if (Array.isArray(set)) return set.includes(value);
27933
+ return value === set;
27934
+ }
27935
+ function toArray(value) {
27936
+ return Array.isArray(value) ? value : [value];
27937
+ }
27938
+ function matchesString(haystack, match) {
27939
+ if (typeof match === "string") return haystack === match;
27940
+ if (match instanceof RegExp) return match.test(haystack);
27941
+ if ("exact" in match) return haystack === match.exact;
27942
+ if ("contains" in match) return haystack.toLowerCase().includes(match.contains.toLowerCase());
27943
+ return false;
27944
+ }
27945
+ //#endregion
27946
+ //#region src/device/zod-to-config-ui.ts
27947
+ /** Access Zod v4 internal .def — not in public typings but stable at runtime */
27948
+ function zodDef(schema) {
27949
+ return schema.def;
27950
+ }
27951
+ /** Access internal properties on a Zod schema instance */
27952
+ function zodInternals(schema) {
27953
+ return schema;
27954
+ }
27955
+ /**
27956
+ * Convert DeviceConfig.entries() output to ConfigUISchema for the admin UI FormBuilder.
27957
+ *
27958
+ * Each entry's Zod type is inspected to determine the correct ConfigField type:
27959
+ * - ZodString → 'text' (or 'password' when key contains "password"/"secret"/"token"/"apikey")
27960
+ * - ZodNumber → 'number' (extracts min/max/step from Zod v4 checks)
27961
+ * - ZodBoolean → 'boolean'
27962
+ * - ZodEnum → 'select' (options built from enum values)
27963
+ * - Anything else → 'text' fallback
27964
+ *
27965
+ * Wrapper types ZodDefault, ZodOptional, and ZodNullable are unwrapped transparently.
27966
+ * Default values are extracted from ZodDefault wrappers.
27967
+ */
27968
+ function zodEntriesToConfigUI(entries, sectionTitle = "Configuration", sectionId = "main") {
27969
+ return { sections: [{
27970
+ id: sectionId,
27971
+ title: sectionTitle,
27972
+ fields: entries.map((entry) => zodToConfigField(entry.key, entry.schema, entry.description))
27973
+ }] };
27974
+ }
27975
+ function zodToConfigField(key, schema, description) {
27976
+ const inner = unwrapZod(schema);
27977
+ const defaultValue = getZodDefault(schema);
27978
+ const base = {
27979
+ key,
27980
+ label: description ?? humanizeKey(key),
27981
+ description,
27982
+ default: defaultValue
27983
+ };
27984
+ if (inner instanceof z.ZodString) return buildStringField(key, base);
27985
+ if (inner instanceof z.ZodNumber) return buildNumberField(inner, base);
27986
+ if (inner instanceof z.ZodBoolean) return {
27987
+ ...base,
27988
+ type: "boolean"
27989
+ };
27990
+ if (inner instanceof z.ZodEnum) return buildEnumField(inner, base);
27991
+ if (inner instanceof z.ZodArray || inner instanceof z.ZodObject) return {
27992
+ ...base,
27993
+ type: "textarea",
27994
+ rows: 6,
27995
+ isJson: true
27996
+ };
27997
+ return {
27998
+ ...base,
27999
+ type: "text"
28000
+ };
28001
+ }
28002
+ function buildStringField(key, base) {
28003
+ const lowerKey = key.toLowerCase();
28004
+ if (lowerKey.includes("password") || lowerKey.includes("secret") || lowerKey.includes("token") || lowerKey.includes("apikey") || lowerKey.includes("api_key")) return {
28005
+ ...base,
28006
+ type: "password",
28007
+ showToggle: true
28008
+ };
28009
+ return {
28010
+ ...base,
28011
+ type: "text"
28012
+ };
28013
+ }
28014
+ function buildNumberField(inner, base) {
28015
+ const anyInner = zodInternals(inner);
28016
+ const rawMin = anyInner.minValue;
28017
+ const rawMax = anyInner.maxValue;
28018
+ const min = rawMin != null && isFinite(rawMin) ? rawMin : void 0;
28019
+ const max = rawMax != null && isFinite(rawMax) ? rawMax : void 0;
28020
+ const step = getMultipleOfStep(inner);
28021
+ return {
28022
+ ...base,
28023
+ type: "number",
28024
+ ...min !== void 0 ? { min } : {},
28025
+ ...max !== void 0 ? { max } : {},
28026
+ ...step !== void 0 ? { step } : {}
28027
+ };
28028
+ }
28029
+ function getMultipleOfStep(inner) {
28030
+ const checks = zodDef(inner).checks ?? [];
28031
+ for (const check of checks) if (check._zod?.def?.check === "multiple_of" && check._zod.def.value !== void 0) return check._zod.def.value;
28032
+ }
28033
+ function buildEnumField(inner, base) {
28034
+ const values = inner.options.map((v) => String(v));
28035
+ return {
28036
+ ...base,
28037
+ type: "select",
28038
+ options: values.map((v) => ({
28039
+ label: humanizeKey(v),
28040
+ value: v
28041
+ }))
28042
+ };
28043
+ }
28044
+ function unwrapZod(schema) {
28045
+ if (schema instanceof z.ZodDefault) return unwrapZod(zodDef(schema).innerType);
28046
+ if (schema instanceof z.ZodOptional) return unwrapZod(zodDef(schema).innerType);
28047
+ if (schema instanceof z.ZodNullable) return unwrapZod(zodDef(schema).innerType);
28048
+ return schema;
28049
+ }
28050
+ function getZodDefault(schema) {
28051
+ if (schema instanceof z.ZodDefault) return zodDef(schema).defaultValue;
28052
+ }
28053
+ function humanizeKey(key) {
28054
+ return key.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/^\w/, (c) => c.toUpperCase()).trim();
28055
+ }
28056
+ //#endregion
28057
+ //#region src/expression/errors.ts
28058
+ /**
28059
+ * Error types for the safe expression engine. Two distinct classes so callers
28060
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
28061
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
28062
+ */
28063
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
28064
+ * the failure is anchored to a character (author-facing inline feedback). */
28065
+ var ExpressionParseError = class extends Error {
28066
+ position;
28067
+ constructor(message, position) {
28068
+ super(message);
28069
+ this.name = "ExpressionParseError";
28070
+ this.position = position;
28071
+ }
28072
+ };
28073
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
28074
+ * result, unknown builtin, step-budget exceeded). */
28075
+ var ExpressionEvalError = class extends Error {
28076
+ constructor(message) {
28077
+ super(message);
28078
+ this.name = "ExpressionEvalError";
28079
+ }
28080
+ };
28081
+ //#endregion
28082
+ //#region src/expression/builtins.ts
28083
+ /**
28084
+ * Frozen, null-prototype builtin function table for the expression engine
28085
+ * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
28086
+ * parser rejects any callee not in it, and the evaluator gates each call on an
28087
+ * own-property check against it.
28088
+ *
28089
+ * Because the object has a NULL prototype AND is `Object.freeze`d:
28090
+ * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
28091
+ * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
28092
+ * (there is no `Object.prototype` in the chain), so those names are not
28093
+ * callable — they are simply "unknown function" at parse time.
28094
+ *
28095
+ * Every numeric argument is validated as a finite number and every numeric
28096
+ * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
28097
+ * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
28098
+ * closed rather than emitting a garbage value.
28099
+ */
28100
+ function asFiniteNumber(value, name, index) {
28101
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
28102
+ return value;
28103
+ }
28104
+ function asString$1(value, name, index) {
28105
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
28106
+ return value;
28107
+ }
28108
+ function finiteResult(value, name) {
28109
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
28110
+ return value;
28111
+ }
28112
+ function allFiniteNumbers(args, name) {
28113
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
28114
+ }
28115
+ var INF = Number.POSITIVE_INFINITY;
28116
+ var table = {
28117
+ min: {
28118
+ minArgs: 1,
28119
+ maxArgs: INF,
28120
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
28121
+ },
28122
+ max: {
28123
+ minArgs: 1,
28124
+ maxArgs: INF,
28125
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
28126
+ },
28127
+ abs: {
28128
+ minArgs: 1,
28129
+ maxArgs: 1,
28130
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
28131
+ },
28132
+ floor: {
28133
+ minArgs: 1,
28134
+ maxArgs: 1,
28135
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
28136
+ },
28137
+ ceil: {
28138
+ minArgs: 1,
28139
+ maxArgs: 1,
28140
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
28141
+ },
28142
+ sqrt: {
28143
+ minArgs: 1,
28144
+ maxArgs: 1,
28145
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
28146
+ },
28147
+ round: {
28148
+ minArgs: 1,
28149
+ maxArgs: 2,
28150
+ apply: (args) => {
28151
+ const x = asFiniteNumber(args[0], "round", 0);
28152
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
28153
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
28154
+ const factor = 10 ** digits;
28155
+ return finiteResult(Math.round(x * factor) / factor, "round");
28156
+ }
28157
+ },
28158
+ pow: {
28159
+ minArgs: 2,
28160
+ maxArgs: 2,
28161
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
28162
+ },
28163
+ clamp: {
28164
+ minArgs: 3,
28165
+ maxArgs: 3,
28166
+ apply: (args) => {
28167
+ const x = asFiniteNumber(args[0], "clamp", 0);
28168
+ const lo = asFiniteNumber(args[1], "clamp", 1);
28169
+ const hi = asFiniteNumber(args[2], "clamp", 2);
28170
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
28171
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
28172
+ }
28173
+ },
28174
+ avg: {
28175
+ minArgs: 1,
28176
+ maxArgs: INF,
28177
+ apply: (args) => {
28178
+ const nums = allFiniteNumbers(args, "avg");
28179
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
28180
+ }
28181
+ },
28182
+ sum: {
28183
+ minArgs: 1,
28184
+ maxArgs: INF,
28185
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
28186
+ },
28187
+ coalesce: {
28188
+ minArgs: 1,
28189
+ maxArgs: INF,
28190
+ apply: (args) => {
28191
+ for (const a of args) if (a !== null) return a;
28192
+ return null;
28193
+ }
28194
+ },
28195
+ age: {
28196
+ minArgs: 2,
28197
+ maxArgs: 2,
28198
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
28199
+ },
28200
+ convert: {
28201
+ minArgs: 3,
28202
+ maxArgs: 3,
28203
+ apply: (args, hooks) => {
28204
+ const x = asFiniteNumber(args[0], "convert", 0);
28205
+ const from = asString$1(args[1], "convert", 1).trim();
28206
+ const to = asString$1(args[2], "convert", 2).trim();
28207
+ if (hooks.convert) {
28208
+ const out = hooks.convert(x, from, to);
28209
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
28210
+ return finiteResult(out, "convert");
28211
+ }
28212
+ if (from === to) return x;
28213
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
28214
+ }
28215
+ }
28216
+ };
28217
+ /** Frozen, null-prototype builtin table. */
28218
+ var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
28219
+ /** The set of valid builtin names — used by the parser to reject unknown
28220
+ * callees at parse time (immediate author feedback). */
28221
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
28222
+ //#endregion
28223
+ //#region src/expression/limits.ts
28224
+ /**
28225
+ * Resource-bound constants for the safe expression engine.
28226
+ *
28227
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
28228
+ * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
28229
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
28230
+ * work a single author-supplied expression can request, so a hostile or
28231
+ * accidental pathological string can never spend unbounded CPU/memory.
28232
+ */
28233
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
28234
+ * rejected without allocation. */
28235
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
28236
+ /** Max AST nodes — checked during parse; a deeply nested grouping that exceeds
28237
+ * this is rejected as "expression too complex". */
28238
+ var MAX_EXPRESSION_AST_NODES = 256;
28239
+ /** Defense-in-depth walker step budget — one increment per node visit during
28240
+ * evaluation. The grammar guarantees O(nodeCount) walks, so this can only trip
28241
+ * on a crafted maximum-size AST. */
28242
+ var MAX_EXPRESSION_EVAL_STEPS = 4096;
28243
+ /** Max named bindings on one {@link ExpressionSource}. */
28244
+ var MAX_EXPRESSION_BINDINGS = 32;
28245
+ /** Max positional arguments to any builtin call. */
28246
+ var MAX_EXPRESSION_CALL_ARGS = 16;
28247
+ /** LRU compile-cache capacity (parsed ASTs keyed by raw source string). */
28248
+ var EXPRESSION_COMPILE_CACHE_CAPACITY = 256;
28249
+ /** A legal binding / identifier name. */
28250
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
28251
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
28252
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
28253
+ var RESERVED_BINDING_NAMES = new Set([
28254
+ "now",
28255
+ "true",
28256
+ "false",
28257
+ "null"
28258
+ ]);
28259
+ //#endregion
28260
+ //#region src/expression/tokenizer.ts
28261
+ /**
28262
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
28263
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
28264
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
28265
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
28266
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
28267
+ * is a parse error with a source position, so member access / assignment /
28268
+ * template literals are lexically impossible.
28269
+ */
28270
+ var KEYWORDS = new Set([
28271
+ "true",
28272
+ "false",
28273
+ "null"
28274
+ ]);
28275
+ function isDigit(ch) {
28276
+ return ch >= "0" && ch <= "9";
28277
+ }
28278
+ function isIdentStart(ch) {
28279
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
28280
+ }
28281
+ function isIdentPart(ch) {
28282
+ return isIdentStart(ch) || isDigit(ch);
28283
+ }
28284
+ function isWhitespace(ch) {
28285
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
28286
+ }
28287
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
28288
+ * Throws `ExpressionParseError` on any illegal character or unterminated
28289
+ * string. */
28290
+ function tokenize(source) {
28291
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
28292
+ const tokens = [];
28293
+ let i = 0;
28294
+ const n = source.length;
28295
+ while (i < n) {
28296
+ const ch = source[i];
28297
+ if (isWhitespace(ch)) {
28298
+ i += 1;
28299
+ continue;
28300
+ }
28301
+ if (isDigit(ch)) {
28302
+ const start = i;
28303
+ while (i < n && isDigit(source[i])) i += 1;
28304
+ if (i < n && source[i] === ".") {
28305
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
28306
+ i += 1;
28307
+ while (i < n && isDigit(source[i])) i += 1;
28308
+ }
28309
+ const text = source.slice(start, i);
28310
+ const value = Number(text);
28311
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
28312
+ tokens.push({
28313
+ type: "number",
28314
+ value,
28315
+ pos: start
28316
+ });
28317
+ continue;
28127
28318
  }
28128
- return out;
28129
- }
28130
- /** First device whose slice matches `predicate`, or null. */
28131
- findState(capName, predicate) {
28132
- for (const [deviceId, perCap] of this.stateMirror) {
28133
- const slice = perCap.get(capName);
28134
- if (!slice) continue;
28135
- if (!predicate(slice, deviceId)) continue;
28136
- return this.getDeviceById(deviceId);
28319
+ if (ch === "'" || ch === "\"") {
28320
+ const quote = ch;
28321
+ const start = i;
28322
+ i += 1;
28323
+ let out = "";
28324
+ let closed = false;
28325
+ while (i < n) {
28326
+ const c = source[i];
28327
+ if (c === "\\") {
28328
+ const next = i + 1 < n ? source[i + 1] : "";
28329
+ if (next === "\\" || next === "'" || next === "\"") {
28330
+ out += next;
28331
+ i += 2;
28332
+ continue;
28333
+ }
28334
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
28335
+ }
28336
+ if (c === quote) {
28337
+ closed = true;
28338
+ i += 1;
28339
+ break;
28340
+ }
28341
+ out += c;
28342
+ i += 1;
28343
+ }
28344
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
28345
+ tokens.push({
28346
+ type: "string",
28347
+ value: out,
28348
+ pos: start
28349
+ });
28350
+ continue;
28137
28351
  }
28138
- return null;
28139
- }
28140
- /** Count devices that bind a cap. Faster than `filterByCap(...).length`. */
28141
- countByCap(capName) {
28142
- let n = 0;
28143
- for (const binding of this.bindings.values()) if (binding.entries.some((e) => e.capName === capName)) n++;
28144
- return n;
28145
- }
28146
- /** Count devices whose slice for `capName` matches `predicate`. */
28147
- countByState(capName, predicate) {
28148
- let n = 0;
28149
- for (const [deviceId, perCap] of this.stateMirror) {
28150
- const slice = perCap.get(capName);
28151
- if (!slice) continue;
28152
- if (predicate(slice, deviceId)) n++;
28352
+ if (isIdentStart(ch)) {
28353
+ const start = i;
28354
+ while (i < n && isIdentPart(source[i])) i += 1;
28355
+ const text = source.slice(start, i);
28356
+ if (KEYWORDS.has(text)) tokens.push({
28357
+ type: "keyword",
28358
+ keyword: keywordOf(text),
28359
+ pos: start
28360
+ });
28361
+ else tokens.push({
28362
+ type: "identifier",
28363
+ name: text,
28364
+ pos: start
28365
+ });
28366
+ continue;
28153
28367
  }
28154
- return n;
28155
- }
28156
- /**
28157
- * Global listener — fires for every `device.state-changed` event the
28158
- * mirror absorbs.
28159
- */
28160
- listen(cb) {
28161
- this.globalStateListeners.add(cb);
28162
- return () => {
28163
- this.globalStateListeners.delete(cb);
28164
- };
28165
- }
28166
- /**
28167
- * Per-cap listener — fires only for state changes on `capName`,
28168
- * across every device. The callback receives the deviceId so the
28169
- * caller can route.
28170
- */
28171
- listenCap(capName, cb) {
28172
- let set = this.capListeners.get(capName);
28173
- if (!set) {
28174
- set = /* @__PURE__ */ new Set();
28175
- this.capListeners.set(capName, set);
28368
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
28369
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
28370
+ tokens.push({
28371
+ type: "punct",
28372
+ punct: two,
28373
+ pos: i
28374
+ });
28375
+ i += 2;
28376
+ continue;
28176
28377
  }
28177
- set.add(cb);
28178
- return () => {
28179
- set.delete(cb);
28180
- if (set.size === 0) this.capListeners.delete(capName);
28181
- };
28182
- }
28183
- /**
28184
- * Per-device listener — fires for every cap change on `deviceId`.
28185
- */
28186
- listenDevice(deviceId, cb) {
28187
- let set = this.deviceListeners.get(deviceId);
28188
- if (!set) {
28189
- set = /* @__PURE__ */ new Set();
28190
- this.deviceListeners.set(deviceId, set);
28378
+ if (isSinglePunct(ch)) {
28379
+ tokens.push({
28380
+ type: "punct",
28381
+ punct: ch,
28382
+ pos: i
28383
+ });
28384
+ i += 1;
28385
+ continue;
28191
28386
  }
28192
- set.add(cb);
28193
- return () => {
28194
- set.delete(cb);
28195
- if (set.size === 0) this.deviceListeners.delete(deviceId);
28196
- };
28387
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
28197
28388
  }
28198
- /** Fires when `device.registered` lands. Receives the new metadata. */
28199
- onDeviceAdded(cb) {
28200
- this.addedListeners.add(cb);
28201
- return () => {
28202
- this.addedListeners.delete(cb);
28203
- };
28389
+ tokens.push({
28390
+ type: "eof",
28391
+ pos: n
28392
+ });
28393
+ return tokens;
28394
+ }
28395
+ function keywordOf(text) {
28396
+ if (text === "true") return "true";
28397
+ if (text === "false") return "false";
28398
+ return "null";
28399
+ }
28400
+ function isSinglePunct(ch) {
28401
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
28402
+ }
28403
+ //#endregion
28404
+ //#region src/expression/parser.ts
28405
+ /**
28406
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
28407
+ *
28408
+ * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
28409
+ * → relational → additive → multiplicative → unary `! -` → call / primary.
28410
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
28411
+ * string validated against the builtin table at parse time, so an unknown
28412
+ * function is rejected immediately (author feedback) and a persisted expression
28413
+ * that references a since-removed builtin degrades at read.
28414
+ *
28415
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
28416
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
28417
+ */
28418
+ /** Binary/logical operator precedence (higher binds tighter). */
28419
+ var BINARY_PRECEDENCE = {
28420
+ "||": 1,
28421
+ "&&": 2,
28422
+ "==": 3,
28423
+ "!=": 3,
28424
+ "<": 4,
28425
+ "<=": 4,
28426
+ ">": 4,
28427
+ ">=": 4,
28428
+ "+": 5,
28429
+ "-": 5,
28430
+ "*": 6,
28431
+ "/": 6,
28432
+ "%": 6
28433
+ };
28434
+ function isLogicalOp(op) {
28435
+ return op === "&&" || op === "||";
28436
+ }
28437
+ function isBinaryOp(op) {
28438
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
28439
+ }
28440
+ var Parser = class {
28441
+ tokens;
28442
+ pos = 0;
28443
+ nodeCount = 0;
28444
+ identifiers = /* @__PURE__ */ new Set();
28445
+ callees = /* @__PURE__ */ new Set();
28446
+ constructor(tokens) {
28447
+ this.tokens = tokens;
28204
28448
  }
28205
- /** Fires when `device.unregistered` lands. `info` is the LAST-known
28206
- * metadata (or null if the device was never seen). */
28207
- onDeviceRemoved(cb) {
28208
- this.removedListeners.add(cb);
28209
- return () => {
28210
- this.removedListeners.delete(cb);
28449
+ parse() {
28450
+ const ast = this.parseTernary();
28451
+ const tok = this.peek();
28452
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
28453
+ return {
28454
+ ast,
28455
+ identifiers: this.identifiers,
28456
+ callees: this.callees,
28457
+ nodeCount: this.nodeCount
28211
28458
  };
28212
28459
  }
28213
- /**
28214
- * Resolve when `predicate` over the runtime-state slice for
28215
- * `(deviceId, capName)` becomes true. Resolves immediately if the
28216
- * current slice already matches. Rejects with `Error('timeout')`
28217
- * after `timeoutMs` (default 30s; pass `Infinity` to wait forever).
28218
- *
28219
- * Returns the matching slice — caller can read it directly without
28220
- * a second mirror lookup.
28221
- */
28222
- waitForState(deviceId, capName, predicate, timeoutMs = 3e4) {
28223
- return new Promise((resolve, reject) => {
28224
- const check = () => {
28225
- const slice = this.stateMirror.get(deviceId)?.get(capName);
28226
- if (slice && predicate(slice)) return slice;
28227
- return null;
28228
- };
28229
- const initial = check();
28230
- if (initial) {
28231
- resolve(initial);
28232
- return;
28233
- }
28234
- let timer = null;
28235
- const off = this.listenDevice(deviceId, (_id, cap, slice) => {
28236
- if (cap !== capName) return;
28237
- if (!slice) return;
28238
- if (predicate(slice)) {
28239
- if (timer) clearTimeout(timer);
28240
- off();
28241
- resolve(slice);
28242
- }
28243
- });
28244
- if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
28245
- off();
28246
- reject(/* @__PURE__ */ new Error(`waitForState timed out after ${timeoutMs}ms (deviceId=${deviceId}, capName=${capName})`));
28247
- }, timeoutMs);
28248
- });
28460
+ peek() {
28461
+ return this.tokens[this.pos];
28249
28462
  }
28250
- /**
28251
- * Resolve when a device with `deviceId` becomes available (a
28252
- * binding exists). Resolves immediately if already known. Rejects
28253
- * with timeout.
28254
- */
28255
- waitForDevice(deviceId, timeoutMs = 3e4) {
28256
- return new Promise((resolve, reject) => {
28257
- const existing = this.getDeviceById(deviceId);
28258
- if (existing) {
28259
- resolve(existing);
28260
- return;
28261
- }
28262
- let timer = null;
28263
- const off = this.onDeviceAdded((id) => {
28264
- if (id !== deviceId) return;
28265
- const proxy = this.getDeviceById(id);
28266
- if (!proxy) return;
28267
- if (timer) clearTimeout(timer);
28268
- off();
28269
- resolve(proxy);
28270
- });
28271
- if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
28272
- off();
28273
- reject(/* @__PURE__ */ new Error(`waitForDevice timed out after ${timeoutMs}ms (deviceId=${deviceId})`));
28274
- }, timeoutMs);
28275
- });
28463
+ next() {
28464
+ return this.tokens[this.pos++];
28276
28465
  }
28277
- /**
28278
- * Iterate every device that binds `capName`. Awaits each callback
28279
- * sequentially for parallel use `invokeCap` with explicit
28280
- * parallelism.
28281
- */
28282
- async forEachCap(capName, cb) {
28283
- for (const proxy of this.filterByCap(capName)) await cb(proxy);
28466
+ /** Consume a punctuator token, erroring if the next token isn't it. */
28467
+ expectPunct(punct) {
28468
+ const tok = this.peek();
28469
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
28470
+ this.pos += 1;
28284
28471
  }
28285
- /**
28286
- * Invoke a cap method on every device that binds the cap. Returns
28287
- * one result per device, success or failure isolated. Optional
28288
- * parallelism cap — useful for "snapshot all cameras but only 4
28289
- * at a time so battery cams don't all wake at once".
28290
- *
28291
- * Example:
28292
- *
28293
- * const results = await sm.invokeCap('snapshot', 'getSnapshot', {}, { parallelism: 4 })
28294
- * const failed = results.filter(r => !r.ok)
28295
- */
28296
- async invokeCap(capName, methodName, args, opts = {}) {
28297
- const targets = this.filterByCap(capName);
28298
- const parallelism = Math.max(1, opts.parallelism ?? targets.length);
28299
- const out = [];
28300
- for (let i = 0; i < targets.length; i += parallelism) {
28301
- const chunk = targets.slice(i, i + parallelism);
28302
- const settled = await Promise.allSettled(chunk.map(async (proxy) => {
28303
- const cap = proxy[capName];
28304
- if (!cap || typeof cap[methodName] !== "function") throw new Error(`device '${proxy.deviceId}' does not expose '${capName}.${methodName}'`);
28305
- return await cap[methodName](args);
28306
- }));
28307
- for (let j = 0; j < settled.length; j++) {
28308
- const proxy = chunk[j];
28309
- const r = settled[j];
28310
- if (r.status === "fulfilled") out.push({
28311
- deviceId: proxy.deviceId,
28312
- ok: true,
28313
- result: r.value
28314
- });
28315
- else out.push({
28316
- deviceId: proxy.deviceId,
28317
- ok: false,
28318
- error: r.reason
28319
- });
28320
- }
28472
+ matchPunct(punct) {
28473
+ const tok = this.peek();
28474
+ if (tok.type === "punct" && tok.punct === punct) {
28475
+ this.pos += 1;
28476
+ return true;
28321
28477
  }
28322
- return out;
28478
+ return false;
28323
28479
  }
28324
- /**
28325
- * One-shot summary — fleet size, breakdown by cap / addon / type.
28326
- * Designed for REPL inspection (`sm.summary()`).
28327
- */
28328
- summary() {
28329
- const byCap = {};
28330
- const byAddon = {};
28331
- const byType = {};
28332
- let online = 0;
28333
- let offline = 0;
28334
- for (const binding of this.bindings.values()) for (const entry of binding.entries) byCap[entry.capName] = (byCap[entry.capName] ?? 0) + 1;
28335
- for (const info of this.devices.values()) {
28336
- byAddon[info.addonId] = (byAddon[info.addonId] ?? 0) + 1;
28337
- byType[info.type] = (byType[info.type] ?? 0) + 1;
28338
- if (info.online) online++;
28339
- else offline++;
28340
- }
28341
- return {
28342
- totalDevices: this.bindings.size,
28343
- online,
28344
- offline,
28345
- byCap,
28346
- byAddon,
28347
- byType,
28348
- statedDevices: this.stateMirror.size
28349
- };
28480
+ countNode() {
28481
+ this.nodeCount += 1;
28482
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
28350
28483
  }
28351
- /**
28352
- * Debug-friendly dump — full state + binding + metadata for one
28353
- * device or all devices. Cheap deep clone so caller mutations don't
28354
- * leak into the mirror.
28355
- */
28356
- dump(deviceId) {
28357
- const dumpOne = (id) => {
28358
- const info = this.devices.get(id) ?? null;
28359
- const binding = this.bindings.get(id) ?? null;
28360
- const state = {};
28361
- const perCap = this.stateMirror.get(id);
28362
- if (perCap) for (const [cap, slice] of perCap) state[cap] = { ...slice };
28484
+ parseTernary() {
28485
+ const test = this.parseBinary(1);
28486
+ if (this.matchPunct("?")) {
28487
+ const consequent = this.parseTernary();
28488
+ this.expectPunct(":");
28489
+ const alternate = this.parseTernary();
28490
+ this.countNode();
28363
28491
  return {
28364
- deviceId: id,
28365
- info,
28366
- binding: binding ? {
28367
- ...binding,
28368
- entries: binding.entries.map((e) => ({ ...e }))
28369
- } : null,
28370
- state
28492
+ kind: "conditional",
28493
+ test,
28494
+ consequent,
28495
+ alternate
28371
28496
  };
28372
- };
28373
- if (deviceId !== void 0) return dumpOne(deviceId);
28374
- const out = [];
28375
- for (const id of this.bindings.keys()) out.push(dumpOne(id));
28376
- return out;
28377
- }
28378
- /**
28379
- * Direct read-only access to the underlying state mirror. Use
28380
- * sparingly — `getSystemState` returns a deep copy that's safer for
28381
- * exploratory work; this avoids the clone cost when iterating
28382
- * thousands of slices.
28383
- */
28384
- getRawMirror() {
28385
- return this.stateMirror;
28497
+ }
28498
+ return test;
28386
28499
  }
28387
- /**
28388
- * Snapshot of the full state mirror — same shape as the warm-boot
28389
- * payload. Deep-cloned; safe to mutate.
28390
- */
28391
- getSystemState() {
28392
- const copy = /* @__PURE__ */ new Map();
28393
- for (const [id, perCap] of this.stateMirror) {
28394
- const dup = /* @__PURE__ */ new Map();
28395
- for (const [k, v] of perCap) dup.set(k, { ...v });
28396
- copy.set(id, dup);
28500
+ parseBinary(minPrec) {
28501
+ let left = this.parseUnary();
28502
+ for (;;) {
28503
+ const tok = this.peek();
28504
+ if (tok.type !== "punct") break;
28505
+ const prec = BINARY_PRECEDENCE[tok.punct];
28506
+ if (prec === void 0 || prec < minPrec) break;
28507
+ const op = tok.punct;
28508
+ this.pos += 1;
28509
+ const right = this.parseBinary(prec + 1);
28510
+ this.countNode();
28511
+ if (isLogicalOp(op)) left = {
28512
+ kind: "logical",
28513
+ op,
28514
+ left,
28515
+ right
28516
+ };
28517
+ else if (isBinaryOp(op)) left = {
28518
+ kind: "binary",
28519
+ op,
28520
+ left,
28521
+ right
28522
+ };
28523
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
28524
+ }
28525
+ return left;
28526
+ }
28527
+ parseUnary() {
28528
+ const tok = this.peek();
28529
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
28530
+ const op = tok.punct;
28531
+ this.pos += 1;
28532
+ const operand = this.parseUnary();
28533
+ this.countNode();
28534
+ return {
28535
+ kind: "unary",
28536
+ op,
28537
+ operand
28538
+ };
28397
28539
  }
28398
- return copy;
28540
+ return this.parsePrimary();
28399
28541
  }
28400
- subscribeBus() {
28401
- if (!this.api.live?.onEvent) return;
28402
- const sub = this.api.live.onEvent;
28403
- this.bridges.push(sub.subscribe({ category: STATE_CHANGED_CATEGORY }, { onData: (evt) => {
28404
- const data = evt.data;
28405
- if (!data || typeof data.deviceId !== "number" || typeof data.capName !== "string") return;
28406
- this.applyStateUpdate(data.deviceId, data.capName, data.slice);
28407
- } }));
28408
- this.bridges.push(sub.subscribe({ category: BINDING_CHANGED_CATEGORY }, { onData: (evt) => {
28409
- const data = evt.data;
28410
- const deviceId = typeof data?.deviceId === "number" ? data.deviceId : data?.source?.type === "device" && typeof data.source.id === "number" ? data.source.id : null;
28411
- if (deviceId === null) return;
28412
- this.refreshBinding(deviceId);
28413
- } }));
28414
- this.bridges.push(sub.subscribe({ category: DEVICE_REGISTERED_CATEGORY }, { onData: (evt) => {
28415
- const data = evt.data;
28416
- if (typeof data?.deviceId !== "number") return;
28417
- this.refreshDeviceMetadata(data.deviceId, "added");
28418
- } }));
28419
- this.bridges.push(sub.subscribe({ category: DEVICE_UNREGISTERED_CATEGORY }, { onData: (evt) => {
28420
- const data = evt.data;
28421
- if (typeof data?.deviceId !== "number") return;
28422
- this.applyDeviceRemoval(data.deviceId);
28423
- } }));
28424
- this.bridges.push(sub.subscribe({ category: DEVICE_UPDATED_CATEGORY }, { onData: (evt) => {
28425
- const data = evt.data;
28426
- if (typeof data?.deviceId !== "number") return;
28427
- this.refreshDeviceMetadata(data.deviceId, "updated");
28428
- } }));
28542
+ parsePrimary() {
28543
+ const tok = this.next();
28544
+ switch (tok.type) {
28545
+ case "number":
28546
+ this.countNode();
28547
+ return {
28548
+ kind: "literal",
28549
+ value: tok.value
28550
+ };
28551
+ case "string":
28552
+ this.countNode();
28553
+ return {
28554
+ kind: "literal",
28555
+ value: tok.value
28556
+ };
28557
+ case "keyword":
28558
+ this.countNode();
28559
+ return {
28560
+ kind: "literal",
28561
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
28562
+ };
28563
+ case "identifier": {
28564
+ const nextTok = this.peek();
28565
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
28566
+ this.identifiers.add(tok.name);
28567
+ this.countNode();
28568
+ return {
28569
+ kind: "identifier",
28570
+ name: tok.name
28571
+ };
28572
+ }
28573
+ case "punct":
28574
+ if (tok.punct === "(") {
28575
+ const inner = this.parseTernary();
28576
+ this.expectPunct(")");
28577
+ return inner;
28578
+ }
28579
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
28580
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
28581
+ }
28429
28582
  }
28430
- applyStateUpdate(deviceId, capName, slice) {
28431
- let perCap = this.stateMirror.get(deviceId);
28432
- if (!perCap) {
28433
- perCap = /* @__PURE__ */ new Map();
28434
- this.stateMirror.set(deviceId, perCap);
28583
+ parseCall(callee, pos) {
28584
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
28585
+ this.expectPunct("(");
28586
+ const args = [];
28587
+ if (!this.matchPunct(")")) for (;;) {
28588
+ args.push(this.parseTernary());
28589
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
28590
+ if (this.matchPunct(",")) continue;
28591
+ this.expectPunct(")");
28592
+ break;
28435
28593
  }
28436
- if (slice === void 0) perCap.delete(capName);
28437
- else perCap.set(capName, slice);
28438
- const handleKey = `${deviceId}:${capName}`;
28439
- const handleSet = this.handleListeners.get(handleKey);
28440
- if (handleSet) for (const cb of handleSet) try {
28441
- cb(slice);
28442
- } catch {}
28443
- for (const cb of this.globalStateListeners) try {
28444
- cb(deviceId, capName, slice);
28445
- } catch {}
28446
- const capSet = this.capListeners.get(capName);
28447
- if (capSet) for (const cb of capSet) try {
28448
- cb(deviceId, slice);
28449
- } catch {}
28450
- const devSet = this.deviceListeners.get(deviceId);
28451
- if (devSet) for (const cb of devSet) try {
28452
- cb(deviceId, capName, slice);
28453
- } catch {}
28594
+ this.callees.add(callee);
28595
+ this.countNode();
28596
+ return {
28597
+ kind: "call",
28598
+ callee,
28599
+ args
28600
+ };
28454
28601
  }
28455
- applyDeviceRemoval(deviceId) {
28456
- const lastInfo = this.devices.get(deviceId) ?? null;
28457
- this.bindings.delete(deviceId);
28458
- this.devices.delete(deviceId);
28459
- this.stateMirror.delete(deviceId);
28460
- for (const cb of this.removedListeners) try {
28461
- cb(deviceId, lastInfo);
28462
- } catch {}
28602
+ };
28603
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
28604
+ * `ExpressionParseError` on any lexical or grammatical failure. */
28605
+ function parseExpression(source) {
28606
+ return new Parser(tokenize(source)).parse();
28607
+ }
28608
+ //#endregion
28609
+ //#region src/expression/compile.ts
28610
+ /**
28611
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
28612
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
28613
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
28614
+ * one per read on a hot resolve path.
28615
+ *
28616
+ * The cache is a module-level singleton: entries are pure, content-addressed
28617
+ * ASTs keyed by the raw source string, so sharing one instance across all
28618
+ * callers is safe and maximises hit rate.
28619
+ */
28620
+ var cache = /* @__PURE__ */ new Map();
28621
+ function getCached(source) {
28622
+ const hit = cache.get(source);
28623
+ if (hit !== void 0) {
28624
+ cache.delete(source);
28625
+ cache.set(source, hit);
28626
+ return hit;
28463
28627
  }
28464
- async refreshBinding(deviceId) {
28465
- try {
28466
- const fresh = (await this.api.deviceManager.getAllBindings.query({})).find((b) => b.deviceId === deviceId);
28467
- if (fresh) this.bindings.set(deviceId, fresh);
28468
- else this.applyDeviceRemoval(deviceId);
28469
- } catch {}
28628
+ let result;
28629
+ try {
28630
+ result = {
28631
+ ok: true,
28632
+ parsed: parseExpression(source)
28633
+ };
28634
+ } catch (err) {
28635
+ result = {
28636
+ ok: false,
28637
+ error: err instanceof ExpressionParseError ? err.message : String(err)
28638
+ };
28470
28639
  }
28471
- async refreshDeviceMetadata(deviceId, kind) {
28472
- try {
28473
- const info = await this.api.deviceManager.getDevice.query({ deviceId });
28474
- if (!info) return;
28475
- const wasNew = !this.devices.has(deviceId);
28476
- this.devices.set(deviceId, info);
28477
- if (kind === "added" && wasNew) {
28478
- await this.refreshBinding(deviceId);
28479
- for (const cb of this.addedListeners) try {
28480
- cb(deviceId, info);
28481
- } catch {}
28482
- }
28483
- } catch {}
28640
+ cache.set(source, result);
28641
+ if (cache.size > 256) {
28642
+ const oldest = cache.keys().next().value;
28643
+ if (oldest !== void 0) cache.delete(oldest);
28484
28644
  }
28485
- };
28486
- function inSet(value, set) {
28487
- if (Array.isArray(set)) return set.includes(value);
28488
- return value === set;
28645
+ return result;
28489
28646
  }
28490
- function toArray(value) {
28491
- return Array.isArray(value) ? value : [value];
28647
+ /** Compile `source` to a `ParsedExpression`, throwing `ExpressionParseError`
28648
+ * on failure. LRU/negative-cached. */
28649
+ function compileExpression(source) {
28650
+ const result = getCached(source);
28651
+ if (result.ok) return result.parsed;
28652
+ throw new ExpressionParseError(result.error);
28492
28653
  }
28493
- function matchesString(haystack, match) {
28494
- if (typeof match === "string") return haystack === match;
28495
- if (match instanceof RegExp) return match.test(haystack);
28496
- if ("exact" in match) return haystack === match.exact;
28497
- if ("contains" in match) return haystack.toLowerCase().includes(match.contains.toLowerCase());
28498
- return false;
28654
+ /** Compile `source`, returning a discriminated result instead of throwing.
28655
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
28656
+ function compileExpressionSafe(source) {
28657
+ return getCached(source);
28499
28658
  }
28500
28659
  //#endregion
28501
- //#region src/device/zod-to-config-ui.ts
28502
- /** Access Zod v4 internal .def — not in public typings but stable at runtime */
28503
- function zodDef(schema) {
28504
- return schema.def;
28505
- }
28506
- /** Access internal properties on a Zod schema instance */
28507
- function zodInternals(schema) {
28508
- return schema;
28509
- }
28660
+ //#region src/expression/evaluator.ts
28510
28661
  /**
28511
- * Convert DeviceConfig.entries() output to ConfigUISchema for the admin UI FormBuilder.
28512
- *
28513
- * Each entry's Zod type is inspected to determine the correct ConfigField type:
28514
- * - ZodString → 'text' (or 'password' when key contains "password"/"secret"/"token"/"apikey")
28515
- * - ZodNumber → 'number' (extracts min/max/step from Zod v4 checks)
28516
- * - ZodBoolean → 'boolean'
28517
- * - ZodEnum → 'select' (options built from enum values)
28518
- * - Anything else → 'text' fallback
28662
+ * Tree-walking evaluator for the safe expression mini-language.
28519
28663
  *
28520
- * Wrapper types ZodDefault, ZodOptional, and ZodNullable are unwrapped transparently.
28521
- * Default values are extracted from ZodDefault wrappers.
28664
+ * SECURITY (spec §4 rule 2/5):
28665
+ * - The scope is an `Object.create(null)` copy of ONLY the caller's own
28666
+ * enumerable binding entries, so `name in scope` is a pure own-key check and
28667
+ * `constructor` / `__proto__` / `toString` are plain unknown identifiers.
28668
+ * - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
28669
+ * directly — the only external calls are into the frozen builtin table.
28670
+ * - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
28671
+ * construction; the step counter is defense-in-depth for a crafted max-size
28672
+ * AST. Nothing blocks: there are no timers, awaits or unbounded loops.
28522
28673
  */
28523
- function zodEntriesToConfigUI(entries, sectionTitle = "Configuration", sectionId = "main") {
28524
- return { sections: [{
28525
- id: sectionId,
28526
- title: sectionTitle,
28527
- fields: entries.map((entry) => zodToConfigField(entry.key, entry.schema, entry.description))
28528
- }] };
28674
+ var EMPTY_HOOKS = Object.freeze({});
28675
+ /** Build a null-prototype scope from own-enumerable binding entries. Inherited
28676
+ * keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
28677
+ * so nothing smuggles in via the prototype chain. */
28678
+ function createExpressionScope(bindings) {
28679
+ const scope = Object.create(null);
28680
+ for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
28681
+ return scope;
28529
28682
  }
28530
- function zodToConfigField(key, schema, description) {
28531
- const inner = unwrapZod(schema);
28532
- const defaultValue = getZodDefault(schema);
28533
- const base = {
28534
- key,
28535
- label: description ?? humanizeKey(key),
28536
- description,
28537
- default: defaultValue
28538
- };
28539
- if (inner instanceof z.ZodString) return buildStringField(key, base);
28540
- if (inner instanceof z.ZodNumber) return buildNumberField(inner, base);
28541
- if (inner instanceof z.ZodBoolean) return {
28542
- ...base,
28543
- type: "boolean"
28544
- };
28545
- if (inner instanceof z.ZodEnum) return buildEnumField(inner, base);
28546
- if (inner instanceof z.ZodArray || inner instanceof z.ZodObject) return {
28547
- ...base,
28548
- type: "textarea",
28549
- rows: 6,
28550
- isJson: true
28551
- };
28552
- return {
28553
- ...base,
28554
- type: "text"
28555
- };
28683
+ function isFiniteNumber(value) {
28684
+ return typeof value === "number" && Number.isFinite(value);
28556
28685
  }
28557
- function buildStringField(key, base) {
28558
- const lowerKey = key.toLowerCase();
28559
- if (lowerKey.includes("password") || lowerKey.includes("secret") || lowerKey.includes("token") || lowerKey.includes("apikey") || lowerKey.includes("api_key")) return {
28560
- ...base,
28561
- type: "password",
28562
- showToggle: true
28563
- };
28564
- return {
28565
- ...base,
28566
- type: "text"
28567
- };
28686
+ /** JS truthiness of a primitive value. */
28687
+ function truthy(value) {
28688
+ return Boolean(value);
28568
28689
  }
28569
- function buildNumberField(inner, base) {
28570
- const anyInner = zodInternals(inner);
28571
- const rawMin = anyInner.minValue;
28572
- const rawMax = anyInner.maxValue;
28573
- const min = rawMin != null && isFinite(rawMin) ? rawMin : void 0;
28574
- const max = rawMax != null && isFinite(rawMax) ? rawMax : void 0;
28575
- const step = getMultipleOfStep(inner);
28576
- return {
28577
- ...base,
28578
- type: "number",
28579
- ...min !== void 0 ? { min } : {},
28580
- ...max !== void 0 ? { max } : {},
28581
- ...step !== void 0 ? { step } : {}
28582
- };
28690
+ function requireFinite(value, context) {
28691
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
28692
+ return value;
28583
28693
  }
28584
- function getMultipleOfStep(inner) {
28585
- const checks = zodDef(inner).checks ?? [];
28586
- for (const check of checks) if (check._zod?.def?.check === "multiple_of" && check._zod.def.value !== void 0) return check._zod.def.value;
28694
+ function step(ctx) {
28695
+ ctx.steps += 1;
28696
+ if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
28587
28697
  }
28588
- function buildEnumField(inner, base) {
28589
- const values = inner.options.map((v) => String(v));
28590
- return {
28591
- ...base,
28592
- type: "select",
28593
- options: values.map((v) => ({
28594
- label: humanizeKey(v),
28595
- value: v
28596
- }))
28597
- };
28698
+ function evalNode(node, ctx) {
28699
+ step(ctx);
28700
+ switch (node.kind) {
28701
+ case "literal": return node.value;
28702
+ case "identifier":
28703
+ if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
28704
+ return ctx.scope[node.name];
28705
+ case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
28706
+ case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
28707
+ case "logical": {
28708
+ const left = evalNode(node.left, ctx);
28709
+ if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
28710
+ return truthy(left) ? left : evalNode(node.right, ctx);
28711
+ }
28712
+ case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
28713
+ case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
28714
+ }
28598
28715
  }
28599
- function unwrapZod(schema) {
28600
- if (schema instanceof z.ZodDefault) return unwrapZod(zodDef(schema).innerType);
28601
- if (schema instanceof z.ZodOptional) return unwrapZod(zodDef(schema).innerType);
28602
- if (schema instanceof z.ZodNullable) return unwrapZod(zodDef(schema).innerType);
28603
- return schema;
28716
+ function evalUnary(op, operand) {
28717
+ if (op === "!") return !truthy(operand);
28718
+ if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
28719
+ return requireFinite(-operand, "unary \"-\"");
28604
28720
  }
28605
- function getZodDefault(schema) {
28606
- if (schema instanceof z.ZodDefault) return zodDef(schema).defaultValue;
28721
+ function evalBinary(op, left, right) {
28722
+ switch (op) {
28723
+ case "==": return left === right;
28724
+ case "!=": return left !== right;
28725
+ case "+":
28726
+ if (typeof left === "string" && typeof right === "string") return left + right;
28727
+ if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
28728
+ throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
28729
+ case "-":
28730
+ case "*":
28731
+ case "/":
28732
+ case "%":
28733
+ if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
28734
+ return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
28735
+ case "<":
28736
+ case "<=":
28737
+ case ">":
28738
+ case ">=":
28739
+ if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
28740
+ if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
28741
+ throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
28742
+ }
28607
28743
  }
28608
- function humanizeKey(key) {
28609
- return key.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/^\w/, (c) => c.toUpperCase()).trim();
28744
+ function evalCall(callee, args, hooks) {
28745
+ if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
28746
+ const builtin = EXPRESSION_BUILTINS[callee];
28747
+ if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
28748
+ return builtin.apply(args, hooks);
28749
+ }
28750
+ /** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
28751
+ * runtime failure (unknown identifier, type mismatch, non-finite result,
28752
+ * step-budget exhaustion). */
28753
+ function evaluateAst(node, scope, opts) {
28754
+ return evalNode(node, {
28755
+ scope,
28756
+ hooks: opts?.hooks ?? EMPTY_HOOKS,
28757
+ maxSteps: opts?.maxSteps ?? 4096,
28758
+ steps: 0
28759
+ });
28760
+ }
28761
+ //#endregion
28762
+ //#region src/expression/expression-source.ts
28763
+ /** The `now` epoch-ms binding is auto-injected into every evaluation and is a
28764
+ * reserved binding name (authors may not rebind it). */
28765
+ var EXPRESSION_INJECTED_NOW = "now";
28766
+ /**
28767
+ * Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
28768
+ * Non-primitive values (objects, arrays, `undefined`, functions, bigint,
28769
+ * symbol) and non-finite numbers become `undefined` so the caller can apply
28770
+ * its binding-miss policy (→ `null`). `null` itself is a valid value.
28771
+ */
28772
+ function toExpressionValue(raw) {
28773
+ if (raw === null) return null;
28774
+ if (typeof raw === "string") return raw;
28775
+ if (typeof raw === "boolean") return raw;
28776
+ if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
28777
+ }
28778
+ /**
28779
+ * Author-time validation. Returns `null` when the source is valid, else a
28780
+ * human-readable error message. Checks: the expression compiles; binding count
28781
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
28782
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
28783
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
28784
+ */
28785
+ function validateExpressionSource(src) {
28786
+ const names = Object.keys(src.bindings);
28787
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
28788
+ for (const name of names) {
28789
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
28790
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
28791
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
28792
+ }
28793
+ const compiled = compileExpressionSafe(src.expr);
28794
+ if (!compiled.ok) return compiled.error;
28795
+ const bound = new Set(names);
28796
+ for (const id of compiled.parsed.identifiers) {
28797
+ if (id === "now") continue;
28798
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
28799
+ }
28800
+ return null;
28801
+ }
28802
+ /**
28803
+ * Shared read-path evaluation. Builds a null-proto scope from `bindingValues`
28804
+ * plus the injected `now` (supplied by the caller for determinism and
28805
+ * testability), compiles via the LRU, and evaluates. Any failure (parse or
28806
+ * eval) returns `{ ok: false }` — the caller treats that as "skip this
28807
+ * derivation", never as a throw that takes the pass down.
28808
+ */
28809
+ function evaluateExpressionSource(expr, bindingValues, now, opts) {
28810
+ const compiled = compileExpressionSafe(expr);
28811
+ if (!compiled.ok) return {
28812
+ ok: false,
28813
+ error: compiled.error
28814
+ };
28815
+ const scope = createExpressionScope({
28816
+ ...bindingValues,
28817
+ ["now"]: now
28818
+ });
28819
+ try {
28820
+ return {
28821
+ ok: true,
28822
+ value: evaluateAst(compiled.parsed.ast, scope, opts)
28823
+ };
28824
+ } catch (err) {
28825
+ return {
28826
+ ok: false,
28827
+ error: err instanceof ExpressionEvalError ? err.message : String(err)
28828
+ };
28829
+ }
28610
28830
  }
28611
28831
  //#endregion
28832
+ //#region src/expression/binding-source.ts
28833
+ /**
28834
+ * What an expression's named bindings READ from.
28835
+ *
28836
+ * Salvaged verbatim from the deleted device-link mechanism. Wiring's source
28837
+ * kinds were the one part of it worth keeping: addressing a device field by
28838
+ * re-sync-stable `stableId`, a per-device constant, and a sibling-accessory
28839
+ * read are the vocabulary any cross-device derivation needs, and they were
28840
+ * already correct. What wiring got wrong was the DESTINATION — a field on
28841
+ * somebody else's device, with no identity — not the source.
28842
+ *
28843
+ * These shapes are therefore kept, re-homed next to the engine that consumes
28844
+ * them, and are the binding type of a composition recipe (the source picker
28845
+ * stays `deviceManager.getWireableFields`). They deliberately do NOT nest: a
28846
+ * binding is a read, never another expression.
28847
+ *
28848
+ * Schemas are authoritative; every type is `z.infer` of one, so a wire shape and
28849
+ * a TypeScript shape cannot drift apart (`scripts/check-schema-type-twins.ts`).
28850
+ */
28851
+ /** Read a sibling accessory's status field, addressed by the sibling's key.
28852
+ * `kind` is optional for wire compatibility — absent means `'field'`. */
28853
+ var ExpressionFieldBindingSchema = z.object({
28854
+ kind: z.literal("field").optional(),
28855
+ sourceKey: z.string(),
28856
+ cap: z.string(),
28857
+ fieldPath: z.string()
28858
+ });
28859
+ /** A constant. No device is read. */
28860
+ var ExpressionLiteralBindingSchema = z.object({
28861
+ kind: z.literal("literal"),
28862
+ value: z.union([
28863
+ z.string(),
28864
+ z.number(),
28865
+ z.boolean(),
28866
+ z.null()
28867
+ ])
28868
+ });
28869
+ /** Read ANY device's status field, addressed by its re-sync-stable `stableId` —
28870
+ * never by numeric id, which a re-adoption reissues. */
28871
+ var ExpressionGlobalBindingSchema = z.object({
28872
+ kind: z.literal("global"),
28873
+ sourceStableId: z.string(),
28874
+ cap: z.string(),
28875
+ fieldPath: z.string()
28876
+ });
28877
+ var ExpressionBindingSourceSchema = z.union([
28878
+ ExpressionFieldBindingSchema,
28879
+ ExpressionLiteralBindingSchema,
28880
+ ExpressionGlobalBindingSchema
28881
+ ]);
28882
+ /**
28883
+ * An expression plus the bindings its free identifiers resolve against.
28884
+ *
28885
+ * The `superRefine` runs the SAME author-time validation as
28886
+ * `validateExpressionSource` — compiles the expression, checks binding names,
28887
+ * checks identifier coverage — so every boundary that parses one
28888
+ * validates-at-write rather than discovering the problem at read time.
28889
+ * Compiles are LRU-cached, so repeated validation of the same string is a hit.
28890
+ */
28891
+ var ExpressionSourceSchema = z.object({
28892
+ expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
28893
+ bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
28894
+ }).superRefine((src, ctx) => {
28895
+ const err = validateExpressionSource(src);
28896
+ if (err !== null) ctx.addIssue({
28897
+ code: "custom",
28898
+ message: err,
28899
+ path: ["expr"]
28900
+ });
28901
+ });
28902
+ //#endregion
28612
28903
  //#region src/generated/cap-status-types.ts
28613
28904
  /**
28614
28905
  * Runtime list of cap names with status. Used by the settings
@@ -30410,6 +30701,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30410
30701
  addonId: null,
30411
30702
  access: "view"
30412
30703
  },
30704
+ "coreBlocks.restart": {
30705
+ capName: "core-blocks",
30706
+ capScope: "system",
30707
+ addonId: null,
30708
+ access: "create"
30709
+ },
30413
30710
  "coreBlocks.setEnabled": {
30414
30711
  capName: "core-blocks",
30415
30712
  capScope: "system",
@@ -31046,12 +31343,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
31046
31343
  addonId: null,
31047
31344
  access: "create"
31048
31345
  },
31049
- "deviceManager.setDeviceLinks": {
31050
- capName: "device-manager",
31051
- capScope: "system",
31052
- addonId: null,
31053
- access: "create"
31054
- },
31055
31346
  "deviceManager.setDisabled": {
31056
31347
  capName: "device-manager",
31057
31348
  capScope: "system",
@@ -35464,6 +35755,7 @@ function createSystemProxy(api) {
35464
35755
  update: (input) => dispatch("coreBlocks", "update", "mutation", input),
35465
35756
  delete: (input) => dispatch("coreBlocks", "delete", "mutation", input),
35466
35757
  setEnabled: (input) => dispatch("coreBlocks", "setEnabled", "mutation", input),
35758
+ restart: (input) => dispatch("coreBlocks", "restart", "mutation", input),
35467
35759
  compile: (input) => dispatch("coreBlocks", "compile", "mutation", input),
35468
35760
  getTypeDefs: (input) => dispatch("coreBlocks", "getTypeDefs", "query", input)
35469
35761
  },
@@ -36387,6 +36679,441 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
36387
36679
  updatedAt: z.number()
36388
36680
  });
36389
36681
  //#endregion
36682
+ //#region src/pipeline/detail-crop.ts
36683
+ /**
36684
+ * THE detail-crop convention — the single derivation of the rectangle a
36685
+ * detail/enrichment step (clip-embedding, face-detection, plate-detection…)
36686
+ * is fed.
36687
+ *
36688
+ * ## Why this is one module and not two constants
36689
+ *
36690
+ * `object-clip` is ONE vector index, and cosine similarity is only meaningful
36691
+ * between vectors produced from the same crop convention. Two encode paths
36692
+ * write into it — the live detail plane and the embedding rebuild — and they
36693
+ * used to derive their crops independently: `DETAIL_CROP_PADDING_RATIO = 0.15`
36694
+ * with no squaring on one side, `DEFAULT_CROP_MARGIN = 0.2` with squaring on
36695
+ * by default on the other. Every rebuild therefore poured a second, silently
36696
+ * incomparable feature space into the index it exists to keep consistent.
36697
+ *
36698
+ * So the rectangle is derived HERE, once, from ONE convention value. Both
36699
+ * paths now reach this function through `pipelineRunner.runDetailSubtree` —
36700
+ * the runner is the only process that cuts (see `detail-subtree.ts`), and the
36701
+ * convention is a cluster-global `pipeline-orchestrator` setting. There is
36702
+ * deliberately no per-node or per-device scope: a per-accelerator crop margin
36703
+ * would reintroduce the same split, merely relocated.
36704
+ *
36705
+ * ## The default IS the live convention
36706
+ *
36707
+ * {@link DEFAULT_DETAIL_CROP_CONVENTION} reproduces what the live path has
36708
+ * been storing (0.15, no squaring). Anything else would invalidate every
36709
+ * vector already in the index on the day it shipped. Changing the convention
36710
+ * is legitimate — that is what the operator knob is for — but it must be
36711
+ * followed by a rebuild, which is now guaranteed to produce crops from this
36712
+ * same function.
36713
+ */
36714
+ /**
36715
+ * Store identity of the convention in `pipeline-orchestrator`'s GLOBAL
36716
+ * (cluster-wide) settings.
36717
+ *
36718
+ * These live here rather than in the orchestrator because the reader is a
36719
+ * different addon — the pipeline runner, over the hub-routed `addon-settings`
36720
+ * cap. Addons never import each other, so a key owned by the writer would have
36721
+ * to be hand-copied by the reader, and a hand-copied key is how a setting
36722
+ * silently stops arriving while both sides still look correct.
36723
+ */
36724
+ var DETAIL_CROP_SECTION_ID = "detail-crop";
36725
+ var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
36726
+ var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
36727
+ /**
36728
+ * Operator-tunable crop convention. Single-valued and cluster-wide — see the
36729
+ * module docblock for why it cannot be scoped per node or per device.
36730
+ */
36731
+ var DetailCropConventionSchema = z.object({
36732
+ /**
36733
+ * Fraction of the box's own size added on EACH side before cutting.
36734
+ *
36735
+ * CLIP is trained on natural images WITH surroundings; a pixel-tight crop
36736
+ * removes exactly the context it is strongest on (a dog cut to its outline
36737
+ * is a dark blob). The right value is an empirical question, which is why it
36738
+ * is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
36739
+ */
36740
+ paddingRatio: z.number().min(0).max(4),
36741
+ /**
36742
+ * Square the window (in PIXELS) before cutting.
36743
+ *
36744
+ * CLIP's input is square, so a tall bbox resized straight to NxN is squashed
36745
+ * — a standing person becomes a shape the model never saw. Squaring costs
36746
+ * extra background, which is context the model wants anyway. Off by default
36747
+ * because the live path has never squared and the stored index reflects that.
36748
+ */
36749
+ square: z.boolean()
36750
+ });
36751
+ /**
36752
+ * The convention in force when nobody has configured one — byte-for-byte the
36753
+ * behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
36754
+ */
36755
+ var DEFAULT_DETAIL_CROP_CONVENTION = {
36756
+ paddingRatio: .15,
36757
+ square: false
36758
+ };
36759
+ /**
36760
+ * Narrow a FLAT settings record to the convention.
36761
+ *
36762
+ * Per-FIELD fallback, deliberately: a junk padding must not also discard a
36763
+ * valid squaring choice. An absent or invalid value resolves to
36764
+ * {@link DEFAULT_DETAIL_CROP_CONVENTION} — the historical live behaviour —
36765
+ * rather than to a clamped number nobody chose, so a bad read can never
36766
+ * quietly change what the stored vectors mean.
36767
+ */
36768
+ function readDetailCropConvention(config) {
36769
+ const paddingRatio = DetailCropConventionSchema.shape.paddingRatio.safeParse(config[DETAIL_CROP_PADDING_KEY]);
36770
+ const square = DetailCropConventionSchema.shape.square.safeParse(config[DETAIL_CROP_SQUARE_KEY]);
36771
+ return {
36772
+ paddingRatio: paddingRatio.success ? paddingRatio.data : DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio,
36773
+ square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
36774
+ };
36775
+ }
36776
+ function isHydratedField$1(entry) {
36777
+ return typeof entry === "object" && entry !== null && "key" in entry;
36778
+ }
36779
+ /**
36780
+ * Extract the convention from an `addon-settings.getGlobalSettings` payload.
36781
+ *
36782
+ * Walks EVERY section rather than looking inside {@link DETAIL_CROP_SECTION_ID}
36783
+ * alone: the keys are unique across the addon's schema, and a section rename
36784
+ * must not silently revert the whole cluster to the default. A `null` payload
36785
+ * (addon mid-boot) is the default convention.
36786
+ */
36787
+ function pickDetailCropConvention(view) {
36788
+ if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
36789
+ const flat = {};
36790
+ for (const section of view.sections) for (const entry of section.fields) {
36791
+ if (!isHydratedField$1(entry) || typeof entry.key !== "string") continue;
36792
+ if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
36793
+ }
36794
+ return readDetailCropConvention(flat);
36795
+ }
36796
+ /** Slider bounds for the operator-facing padding knob (orchestrator settings UI). */
36797
+ var DETAIL_CROP_PADDING_FIELD = {
36798
+ min: 0,
36799
+ max: 1,
36800
+ step: .05,
36801
+ default: DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio
36802
+ };
36803
+ /**
36804
+ * Derive the crop rectangle for one parent detection.
36805
+ *
36806
+ * Order: pad by `paddingRatio` of the box's own size → optionally square in
36807
+ * pixel space around the padded centre → keep it inside the frame. Pure:
36808
+ * always returns a new rect and never mutates `bbox`.
36809
+ *
36810
+ * Edge handling differs by mode, on purpose:
36811
+ *
36812
+ * - **unsquared** — TRUNCATED at the frame border, byte-for-byte what the live
36813
+ * path has always done (`padAndClampFrameBbox`). A subject against the edge
36814
+ * gets a slightly smaller window. Changing this would silently reinterpret
36815
+ * every edge-touching vector already in the index.
36816
+ * - **squared** — SLID inward instead, because a truncated square is not
36817
+ * square and squaring exists precisely to preserve the aspect the model
36818
+ * sees. It only shrinks when the square is larger than the frame itself.
36819
+ */
36820
+ function deriveDetailCropRect(bbox, frameWidth, frameHeight, convention) {
36821
+ const padX = convention.paddingRatio * bbox.w;
36822
+ const padY = convention.paddingRatio * bbox.h;
36823
+ const padded = {
36824
+ x: bbox.x - padX,
36825
+ y: bbox.y - padY,
36826
+ w: bbox.w + 2 * padX,
36827
+ h: bbox.h + 2 * padY
36828
+ };
36829
+ return convention.square ? slideInsideFrame(squareInPixels(padded, frameWidth, frameHeight), frameWidth, frameHeight) : truncateToFrame(padded, frameWidth, frameHeight);
36830
+ }
36831
+ /**
36832
+ * Grow the shorter side to the longer one around the window's centre, bounded
36833
+ * by the frame's shorter side — a square larger than the frame cannot exist,
36834
+ * and collapsing to the frame's short side is the most that does.
36835
+ */
36836
+ function squareInPixels(rect, frameWidth, frameHeight) {
36837
+ const side = Math.min(Math.max(rect.w, rect.h), Math.min(frameWidth, frameHeight));
36838
+ const cx = rect.x + rect.w / 2;
36839
+ const cy = rect.y + rect.h / 2;
36840
+ return {
36841
+ x: cx - side / 2,
36842
+ y: cy - side / 2,
36843
+ w: side,
36844
+ h: side
36845
+ };
36846
+ }
36847
+ /**
36848
+ * Cut the window at the frame border — the pre-unification live behaviour,
36849
+ * preserved exactly so unsquared crops keep matching the stored index.
36850
+ */
36851
+ function truncateToFrame(rect, frameWidth, frameHeight) {
36852
+ const x1 = Math.max(0, rect.x);
36853
+ const y1 = Math.max(0, rect.y);
36854
+ const x2 = Math.min(frameWidth, rect.x + rect.w);
36855
+ const y2 = Math.min(frameHeight, rect.y + rect.h);
36856
+ return {
36857
+ x: x1,
36858
+ y: y1,
36859
+ w: Math.max(0, x2 - x1),
36860
+ h: Math.max(0, y2 - y1)
36861
+ };
36862
+ }
36863
+ /**
36864
+ * Move the window inside the frame keeping its extent — used only for squared
36865
+ * windows, where truncating would destroy the squareness that is the point.
36866
+ */
36867
+ function slideInsideFrame(rect, frameWidth, frameHeight) {
36868
+ const w = Math.max(0, Math.min(rect.w, frameWidth));
36869
+ const h = Math.max(0, Math.min(rect.h, frameHeight));
36870
+ return {
36871
+ x: Math.min(Math.max(0, rect.x), Math.max(0, frameWidth - w)),
36872
+ y: Math.min(Math.max(0, rect.y), Math.max(0, frameHeight - h)),
36873
+ w,
36874
+ h
36875
+ };
36876
+ }
36877
+ //#endregion
36878
+ //#region src/pipeline/native-lease.ts
36879
+ /**
36880
+ * THE native-frame **lease** knobs — TTL, RAM budget and demand window for the
36881
+ * decode worker's native-resolution frame retention.
36882
+ *
36883
+ * ## Why they live here and not in the addon that reads them
36884
+ *
36885
+ * The WRITER is `pipeline-orchestrator` (the cluster-wide settings authority);
36886
+ * the READER is a private child process of `addon-pipeline`'s pipeline-runner.
36887
+ * Addons never import each other, so a key owned by either side would have to
36888
+ * be hand-copied by the other — and a hand-copied key is how a setting silently
36889
+ * stops arriving while both sides still look correct. Same reasoning, same
36890
+ * placement as `detail-crop.ts` (D52's "one cluster-wide orchestrator setting").
36891
+ *
36892
+ * ## Why cluster-wide and not per-node
36893
+ *
36894
+ * The lease is a per-decode-worker RAM window. Its purpose — the late
36895
+ * cross-process native crop landing on a full-resolution frame rather than the
36896
+ * ≤640 detection fallback — is a property of the PIPELINE, not of a node's
36897
+ * hardware: a per-node TTL would mean the same camera produces different crop
36898
+ * quality depending on which node the balancer placed it on, and nobody could
36899
+ * tell that from the stored media. Node-level RAM pressure is already handled
36900
+ * by the per-session budget ceiling, which is itself one of these knobs.
36901
+ *
36902
+ * ## What each knob costs
36903
+ *
36904
+ * A retained frame is a full NATIVE-resolution copy in system RAM. With the
36905
+ * default pinned-RGB24 lease path (`CAMSTACK_SESSION_PINNED_RGB_CROP`, on):
36906
+ * 4K ≈ 24.9 MB/frame, 1080p ≈ 6.2 MB/frame. On the YUV420P path (flag off, and
36907
+ * for software-decoded sessions): 4K ≈ 12.4 MB, 1080p ≈ 3.1 MB. Worst-case
36908
+ * resident RAM for ONE busy camera ≈ frameBytes × deliveredFps × ttlSeconds,
36909
+ * clamped by the budget ceiling. See `docs/design/decode-path.md` → "Lease
36910
+ * admission" for what actually gets admitted.
36911
+ */
36912
+ /**
36913
+ * Store identity of the lease knobs in `pipeline-orchestrator`'s GLOBAL
36914
+ * (cluster-wide) settings. Keys are unique across that addon's whole schema, so
36915
+ * the reader can walk every section instead of trusting the section id.
36916
+ */
36917
+ var NATIVE_LEASE_SECTION_ID = "native-lease";
36918
+ var NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
36919
+ var NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
36920
+ var NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
36921
+ var NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
36922
+ /**
36923
+ * WHICH delivered frames the decode worker retains a native copy of.
36924
+ *
36925
+ * - `all` — every frame the worker delivered to the runner. The shipped
36926
+ * behaviour, and the only correct one if something can ask for a crop of a
36927
+ * frame the runner never sent to inference.
36928
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
36929
+ * A native-crop request always names a `frameId` that rode an inference
36930
+ * result, so that is the only set a request can name. How much it drops is
36931
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
36932
+ * this cluster, not the ~80% the design sketch assumed, because the governor
36933
+ * was not throttling as hard as the sketch supposed. Read
36934
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
36935
+ * of you rather than quoting a number from here. The newest delivered frame is
36936
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
36937
+ * which covers the one-frame race between a mark and the supersede that
36938
+ * consumes it.
36939
+ */
36940
+ var NativeLeaseAdmissionSchema = z.enum(["all", "inferred"]);
36941
+ /**
36942
+ * Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
36943
+ * the slider) because the value also travels to a forked child process, where a
36944
+ * junk number would silently become a 0-length or unbounded retention window.
36945
+ */
36946
+ var NativeLeaseSettingsSchema = z.object({
36947
+ /**
36948
+ * How long a retained native frame is served before it counts as a miss.
36949
+ *
36950
+ * Must cover the FULL late-crop horizon: detection inference + the
36951
+ * cross-process inference-result hop to hub post-analysis + tracking + the
36952
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
36953
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
36954
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
36955
+ */
36956
+ ttlMs: z.number().int().min(250).max(1e4),
36957
+ /**
36958
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
36959
+ *
36960
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
36961
+ * which one is actually binding before reasoning from that. At the shipped
36962
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
36963
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
36964
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
36965
+ * change that admits fewer frames buys retention WINDOW at constant RAM
36966
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
36967
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
36968
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
36969
+ * to replace).
36970
+ */
36971
+ budgetMb: z.number().int().min(0).max(4096),
36972
+ /**
36973
+ * Demand window: eager per-frame native retention runs only within this many
36974
+ * ms of the last native-crop request (or of the dial starting).
36975
+ *
36976
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
36977
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
36978
+ * per second on a camera with zero crop demand), so leave it non-zero unless
36979
+ * you are reproducing that.
36980
+ */
36981
+ activityMs: z.number().int().min(0).max(12e4),
36982
+ /**
36983
+ * Which delivered frames are retained at all — see
36984
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
36985
+ * changes WHAT is kept rather than for how long, so it is also the only one
36986
+ * that can turn a crop that used to hit into a miss. The worker counts every
36987
+ * crop request naming a frame it did NOT see marked
36988
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
36989
+ * there is the signal that some caller names frames outside the inference set
36990
+ * and that this must go back to `all`.
36991
+ */
36992
+ admission: NativeLeaseAdmissionSchema
36993
+ });
36994
+ /**
36995
+ * The values in force when the operator has set nothing — byte-for-byte the
36996
+ * constants the decode worker shipped with as env-var defaults, so making these
36997
+ * settings changed no behaviour on the day it landed.
36998
+ */
36999
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
37000
+ ttlMs: 1200,
37001
+ budgetMb: 1024,
37002
+ activityMs: 15e3,
37003
+ admission: "inferred"
37004
+ };
37005
+ /** Slider bounds for the operator-facing knobs (orchestrator settings UI). */
37006
+ var NATIVE_LEASE_TTL_FIELD = {
37007
+ min: 250,
37008
+ max: 1e4,
37009
+ step: 50,
37010
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs
37011
+ };
37012
+ var NATIVE_LEASE_BUDGET_FIELD = {
37013
+ min: 0,
37014
+ max: 4096,
37015
+ step: 64,
37016
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb
37017
+ };
37018
+ var NATIVE_LEASE_ACTIVITY_FIELD = {
37019
+ min: 0,
37020
+ max: 12e4,
37021
+ step: 1e3,
37022
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.activityMs
37023
+ };
37024
+ /** Select options for the admission knob (orchestrator settings UI). */
37025
+ var NATIVE_LEASE_ADMISSION_FIELD = {
37026
+ options: [{
37027
+ value: "all",
37028
+ label: "Every delivered frame"
37029
+ }, {
37030
+ value: "inferred",
37031
+ label: "Only frames sent to inference"
37032
+ }],
37033
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.admission
37034
+ };
37035
+ /**
37036
+ * Parse one knob, reporting `null` for absent, junk, out-of-bounds — AND for a
37037
+ * value equal to the shipped default.
37038
+ *
37039
+ * That last rule is not tidiness, it is the difference between the documented
37040
+ * precedence being true and being a lie. `addon-settings.getGlobalSettings`
37041
+ * returns a HYDRATED payload, and `hydrateField` fills an unstored field with
37042
+ * the schema's own `default` (verified live on the hub: a cluster that has never
37043
+ * opened the form still reports `nativeLeaseTtlMs = 1200`). A reader that took
37044
+ * that at face value would report all three knobs as "set" on every cluster on
37045
+ * the day this shipped, permanently retiring the `CAMSTACK_SESSION_NATIVE_LEASE_*`
37046
+ * emergency override that the precedence promises. There is no raw-store read on
37047
+ * this cap to distinguish the two, so the default value itself is treated as
37048
+ * "the operator has expressed no preference" — which is also what leaving a
37049
+ * slider untouched means.
37050
+ *
37051
+ * The cost is one honest edge: an operator who deliberately selects the default
37052
+ * value in order to overrule an env var does not get it. Clear the env var
37053
+ * instead; the worker's spawn line names the source, so this is visible rather
37054
+ * than mysterious.
37055
+ */
37056
+ function readKnob(knob, raw) {
37057
+ const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(raw);
37058
+ if (!parsed.success) return null;
37059
+ return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS[knob] ? null : parsed.data;
37060
+ }
37061
+ /** {@link readKnob} for the one non-numeric knob. Same default-means-unset rule. */
37062
+ function readAdmissionKnob(raw) {
37063
+ const parsed = NativeLeaseAdmissionSchema.safeParse(raw);
37064
+ if (!parsed.success) return null;
37065
+ return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS.admission ? null : parsed.data;
37066
+ }
37067
+ /**
37068
+ * Narrow a FLAT settings record to the knobs the operator set.
37069
+ *
37070
+ * Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
37071
+ * budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
37072
+ * clamped, not defaulted) so the caller can still fall through to the env
37073
+ * override — clamping here would turn a typo into a value nobody chose. See
37074
+ * {@link readKnob} for why the default counts as unset.
37075
+ */
37076
+ function readNativeLeaseOverride(config) {
37077
+ const ttlMs = readKnob("ttlMs", config[NATIVE_LEASE_TTL_KEY]);
37078
+ const budgetMb = readKnob("budgetMb", config[NATIVE_LEASE_BUDGET_KEY]);
37079
+ const activityMs = readKnob("activityMs", config[NATIVE_LEASE_ACTIVITY_KEY]);
37080
+ const admission = readAdmissionKnob(config[NATIVE_LEASE_ADMISSION_KEY]);
37081
+ return {
37082
+ ...ttlMs === null ? {} : { ttlMs },
37083
+ ...budgetMb === null ? {} : { budgetMb },
37084
+ ...activityMs === null ? {} : { activityMs },
37085
+ ...admission === null ? {} : { admission }
37086
+ };
37087
+ }
37088
+ function isHydratedField(entry) {
37089
+ return typeof entry === "object" && entry !== null && "key" in entry;
37090
+ }
37091
+ var LEASE_KEYS = [
37092
+ NATIVE_LEASE_TTL_KEY,
37093
+ NATIVE_LEASE_BUDGET_KEY,
37094
+ NATIVE_LEASE_ACTIVITY_KEY,
37095
+ NATIVE_LEASE_ADMISSION_KEY
37096
+ ];
37097
+ /**
37098
+ * Extract the operator's lease overrides from an
37099
+ * `addon-settings.getGlobalSettings` payload.
37100
+ *
37101
+ * Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
37102
+ * alone: the keys are unique across the addon's schema, and a section rename
37103
+ * must not silently revert the whole cluster to the defaults. A `null` payload
37104
+ * (addon mid-boot) means "operator set nothing" — the env/default fallback then
37105
+ * applies, which is the correct read of "I could not ask".
37106
+ */
37107
+ function pickNativeLeaseOverride(view) {
37108
+ if (view === null) return {};
37109
+ const flat = {};
37110
+ for (const section of view.sections) for (const entry of section.fields) {
37111
+ if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
37112
+ if (LEASE_KEYS.includes(entry.key)) flat[entry.key] = entry.value;
37113
+ }
37114
+ return readNativeLeaseOverride(flat);
37115
+ }
37116
+ //#endregion
36390
37117
  //#region src/types/device-type.ts
36391
37118
  var DEVICE_TYPE_INFO = { ["camera"]: {
36392
37119
  type: "camera",
@@ -37446,441 +38173,6 @@ function pointInPolygon(point, polygon) {
37446
38173
  return inside;
37447
38174
  }
37448
38175
  //#endregion
37449
- //#region src/pipeline/detail-crop.ts
37450
- /**
37451
- * THE detail-crop convention — the single derivation of the rectangle a
37452
- * detail/enrichment step (clip-embedding, face-detection, plate-detection…)
37453
- * is fed.
37454
- *
37455
- * ## Why this is one module and not two constants
37456
- *
37457
- * `object-clip` is ONE vector index, and cosine similarity is only meaningful
37458
- * between vectors produced from the same crop convention. Two encode paths
37459
- * write into it — the live detail plane and the embedding rebuild — and they
37460
- * used to derive their crops independently: `DETAIL_CROP_PADDING_RATIO = 0.15`
37461
- * with no squaring on one side, `DEFAULT_CROP_MARGIN = 0.2` with squaring on
37462
- * by default on the other. Every rebuild therefore poured a second, silently
37463
- * incomparable feature space into the index it exists to keep consistent.
37464
- *
37465
- * So the rectangle is derived HERE, once, from ONE convention value. Both
37466
- * paths now reach this function through `pipelineRunner.runDetailSubtree` —
37467
- * the runner is the only process that cuts (see `detail-subtree.ts`), and the
37468
- * convention is a cluster-global `pipeline-orchestrator` setting. There is
37469
- * deliberately no per-node or per-device scope: a per-accelerator crop margin
37470
- * would reintroduce the same split, merely relocated.
37471
- *
37472
- * ## The default IS the live convention
37473
- *
37474
- * {@link DEFAULT_DETAIL_CROP_CONVENTION} reproduces what the live path has
37475
- * been storing (0.15, no squaring). Anything else would invalidate every
37476
- * vector already in the index on the day it shipped. Changing the convention
37477
- * is legitimate — that is what the operator knob is for — but it must be
37478
- * followed by a rebuild, which is now guaranteed to produce crops from this
37479
- * same function.
37480
- */
37481
- /**
37482
- * Store identity of the convention in `pipeline-orchestrator`'s GLOBAL
37483
- * (cluster-wide) settings.
37484
- *
37485
- * These live here rather than in the orchestrator because the reader is a
37486
- * different addon — the pipeline runner, over the hub-routed `addon-settings`
37487
- * cap. Addons never import each other, so a key owned by the writer would have
37488
- * to be hand-copied by the reader, and a hand-copied key is how a setting
37489
- * silently stops arriving while both sides still look correct.
37490
- */
37491
- var DETAIL_CROP_SECTION_ID = "detail-crop";
37492
- var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
37493
- var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
37494
- /**
37495
- * Operator-tunable crop convention. Single-valued and cluster-wide — see the
37496
- * module docblock for why it cannot be scoped per node or per device.
37497
- */
37498
- var DetailCropConventionSchema = z.object({
37499
- /**
37500
- * Fraction of the box's own size added on EACH side before cutting.
37501
- *
37502
- * CLIP is trained on natural images WITH surroundings; a pixel-tight crop
37503
- * removes exactly the context it is strongest on (a dog cut to its outline
37504
- * is a dark blob). The right value is an empirical question, which is why it
37505
- * is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
37506
- */
37507
- paddingRatio: z.number().min(0).max(4),
37508
- /**
37509
- * Square the window (in PIXELS) before cutting.
37510
- *
37511
- * CLIP's input is square, so a tall bbox resized straight to NxN is squashed
37512
- * — a standing person becomes a shape the model never saw. Squaring costs
37513
- * extra background, which is context the model wants anyway. Off by default
37514
- * because the live path has never squared and the stored index reflects that.
37515
- */
37516
- square: z.boolean()
37517
- });
37518
- /**
37519
- * The convention in force when nobody has configured one — byte-for-byte the
37520
- * behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
37521
- */
37522
- var DEFAULT_DETAIL_CROP_CONVENTION = {
37523
- paddingRatio: .15,
37524
- square: false
37525
- };
37526
- /**
37527
- * Narrow a FLAT settings record to the convention.
37528
- *
37529
- * Per-FIELD fallback, deliberately: a junk padding must not also discard a
37530
- * valid squaring choice. An absent or invalid value resolves to
37531
- * {@link DEFAULT_DETAIL_CROP_CONVENTION} — the historical live behaviour —
37532
- * rather than to a clamped number nobody chose, so a bad read can never
37533
- * quietly change what the stored vectors mean.
37534
- */
37535
- function readDetailCropConvention(config) {
37536
- const paddingRatio = DetailCropConventionSchema.shape.paddingRatio.safeParse(config[DETAIL_CROP_PADDING_KEY]);
37537
- const square = DetailCropConventionSchema.shape.square.safeParse(config[DETAIL_CROP_SQUARE_KEY]);
37538
- return {
37539
- paddingRatio: paddingRatio.success ? paddingRatio.data : DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio,
37540
- square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
37541
- };
37542
- }
37543
- function isHydratedField$1(entry) {
37544
- return typeof entry === "object" && entry !== null && "key" in entry;
37545
- }
37546
- /**
37547
- * Extract the convention from an `addon-settings.getGlobalSettings` payload.
37548
- *
37549
- * Walks EVERY section rather than looking inside {@link DETAIL_CROP_SECTION_ID}
37550
- * alone: the keys are unique across the addon's schema, and a section rename
37551
- * must not silently revert the whole cluster to the default. A `null` payload
37552
- * (addon mid-boot) is the default convention.
37553
- */
37554
- function pickDetailCropConvention(view) {
37555
- if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
37556
- const flat = {};
37557
- for (const section of view.sections) for (const entry of section.fields) {
37558
- if (!isHydratedField$1(entry) || typeof entry.key !== "string") continue;
37559
- if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
37560
- }
37561
- return readDetailCropConvention(flat);
37562
- }
37563
- /** Slider bounds for the operator-facing padding knob (orchestrator settings UI). */
37564
- var DETAIL_CROP_PADDING_FIELD = {
37565
- min: 0,
37566
- max: 1,
37567
- step: .05,
37568
- default: DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio
37569
- };
37570
- /**
37571
- * Derive the crop rectangle for one parent detection.
37572
- *
37573
- * Order: pad by `paddingRatio` of the box's own size → optionally square in
37574
- * pixel space around the padded centre → keep it inside the frame. Pure:
37575
- * always returns a new rect and never mutates `bbox`.
37576
- *
37577
- * Edge handling differs by mode, on purpose:
37578
- *
37579
- * - **unsquared** — TRUNCATED at the frame border, byte-for-byte what the live
37580
- * path has always done (`padAndClampFrameBbox`). A subject against the edge
37581
- * gets a slightly smaller window. Changing this would silently reinterpret
37582
- * every edge-touching vector already in the index.
37583
- * - **squared** — SLID inward instead, because a truncated square is not
37584
- * square and squaring exists precisely to preserve the aspect the model
37585
- * sees. It only shrinks when the square is larger than the frame itself.
37586
- */
37587
- function deriveDetailCropRect(bbox, frameWidth, frameHeight, convention) {
37588
- const padX = convention.paddingRatio * bbox.w;
37589
- const padY = convention.paddingRatio * bbox.h;
37590
- const padded = {
37591
- x: bbox.x - padX,
37592
- y: bbox.y - padY,
37593
- w: bbox.w + 2 * padX,
37594
- h: bbox.h + 2 * padY
37595
- };
37596
- return convention.square ? slideInsideFrame(squareInPixels(padded, frameWidth, frameHeight), frameWidth, frameHeight) : truncateToFrame(padded, frameWidth, frameHeight);
37597
- }
37598
- /**
37599
- * Grow the shorter side to the longer one around the window's centre, bounded
37600
- * by the frame's shorter side — a square larger than the frame cannot exist,
37601
- * and collapsing to the frame's short side is the most that does.
37602
- */
37603
- function squareInPixels(rect, frameWidth, frameHeight) {
37604
- const side = Math.min(Math.max(rect.w, rect.h), Math.min(frameWidth, frameHeight));
37605
- const cx = rect.x + rect.w / 2;
37606
- const cy = rect.y + rect.h / 2;
37607
- return {
37608
- x: cx - side / 2,
37609
- y: cy - side / 2,
37610
- w: side,
37611
- h: side
37612
- };
37613
- }
37614
- /**
37615
- * Cut the window at the frame border — the pre-unification live behaviour,
37616
- * preserved exactly so unsquared crops keep matching the stored index.
37617
- */
37618
- function truncateToFrame(rect, frameWidth, frameHeight) {
37619
- const x1 = Math.max(0, rect.x);
37620
- const y1 = Math.max(0, rect.y);
37621
- const x2 = Math.min(frameWidth, rect.x + rect.w);
37622
- const y2 = Math.min(frameHeight, rect.y + rect.h);
37623
- return {
37624
- x: x1,
37625
- y: y1,
37626
- w: Math.max(0, x2 - x1),
37627
- h: Math.max(0, y2 - y1)
37628
- };
37629
- }
37630
- /**
37631
- * Move the window inside the frame keeping its extent — used only for squared
37632
- * windows, where truncating would destroy the squareness that is the point.
37633
- */
37634
- function slideInsideFrame(rect, frameWidth, frameHeight) {
37635
- const w = Math.max(0, Math.min(rect.w, frameWidth));
37636
- const h = Math.max(0, Math.min(rect.h, frameHeight));
37637
- return {
37638
- x: Math.min(Math.max(0, rect.x), Math.max(0, frameWidth - w)),
37639
- y: Math.min(Math.max(0, rect.y), Math.max(0, frameHeight - h)),
37640
- w,
37641
- h
37642
- };
37643
- }
37644
- //#endregion
37645
- //#region src/pipeline/native-lease.ts
37646
- /**
37647
- * THE native-frame **lease** knobs — TTL, RAM budget and demand window for the
37648
- * decode worker's native-resolution frame retention.
37649
- *
37650
- * ## Why they live here and not in the addon that reads them
37651
- *
37652
- * The WRITER is `pipeline-orchestrator` (the cluster-wide settings authority);
37653
- * the READER is a private child process of `addon-pipeline`'s pipeline-runner.
37654
- * Addons never import each other, so a key owned by either side would have to
37655
- * be hand-copied by the other — and a hand-copied key is how a setting silently
37656
- * stops arriving while both sides still look correct. Same reasoning, same
37657
- * placement as `detail-crop.ts` (D52's "one cluster-wide orchestrator setting").
37658
- *
37659
- * ## Why cluster-wide and not per-node
37660
- *
37661
- * The lease is a per-decode-worker RAM window. Its purpose — the late
37662
- * cross-process native crop landing on a full-resolution frame rather than the
37663
- * ≤640 detection fallback — is a property of the PIPELINE, not of a node's
37664
- * hardware: a per-node TTL would mean the same camera produces different crop
37665
- * quality depending on which node the balancer placed it on, and nobody could
37666
- * tell that from the stored media. Node-level RAM pressure is already handled
37667
- * by the per-session budget ceiling, which is itself one of these knobs.
37668
- *
37669
- * ## What each knob costs
37670
- *
37671
- * A retained frame is a full NATIVE-resolution copy in system RAM. With the
37672
- * default pinned-RGB24 lease path (`CAMSTACK_SESSION_PINNED_RGB_CROP`, on):
37673
- * 4K ≈ 24.9 MB/frame, 1080p ≈ 6.2 MB/frame. On the YUV420P path (flag off, and
37674
- * for software-decoded sessions): 4K ≈ 12.4 MB, 1080p ≈ 3.1 MB. Worst-case
37675
- * resident RAM for ONE busy camera ≈ frameBytes × deliveredFps × ttlSeconds,
37676
- * clamped by the budget ceiling. See `docs/design/decode-path.md` → "Lease
37677
- * admission" for what actually gets admitted.
37678
- */
37679
- /**
37680
- * Store identity of the lease knobs in `pipeline-orchestrator`'s GLOBAL
37681
- * (cluster-wide) settings. Keys are unique across that addon's whole schema, so
37682
- * the reader can walk every section instead of trusting the section id.
37683
- */
37684
- var NATIVE_LEASE_SECTION_ID = "native-lease";
37685
- var NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
37686
- var NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
37687
- var NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
37688
- var NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
37689
- /**
37690
- * WHICH delivered frames the decode worker retains a native copy of.
37691
- *
37692
- * - `all` — every frame the worker delivered to the runner. The shipped
37693
- * behaviour, and the only correct one if something can ask for a crop of a
37694
- * frame the runner never sent to inference.
37695
- * - `inferred` — only the frames the runner ADMITTED to its detection queue.
37696
- * A native-crop request always names a `frameId` that rode an inference
37697
- * result, so that is the only set a request can name. How much it drops is
37698
- * the two-plane governor's admit ratio and nothing else: measured at ~50% on
37699
- * this cluster, not the ~80% the design sketch assumed, because the governor
37700
- * was not throttling as hard as the sketch supposed. Read
37701
- * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
37702
- * of you rather than quoting a number from here. The newest delivered frame is
37703
- * croppable regardless — it is still the worker's reserved slot, not a lease —
37704
- * which covers the one-frame race between a mark and the supersede that
37705
- * consumes it.
37706
- */
37707
- var NativeLeaseAdmissionSchema = z.enum(["all", "inferred"]);
37708
- /**
37709
- * Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
37710
- * the slider) because the value also travels to a forked child process, where a
37711
- * junk number would silently become a 0-length or unbounded retention window.
37712
- */
37713
- var NativeLeaseSettingsSchema = z.object({
37714
- /**
37715
- * How long a retained native frame is served before it counts as a miss.
37716
- *
37717
- * Must cover the FULL late-crop horizon: detection inference + the
37718
- * cross-process inference-result hop to hub post-analysis + tracking + the
37719
- * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
37720
- * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
37721
- * RAM per busy camera grows linearly with no measured hit-rate gain.
37722
- */
37723
- ttlMs: z.number().int().min(250).max(1e4),
37724
- /**
37725
- * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
37726
- *
37727
- * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
37728
- * which one is actually binding before reasoning from that. At the shipped
37729
- * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
37730
- * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
37731
- * `leaseFrames` on the metrics line say which. When the ceiling binds, a
37732
- * change that admits fewer frames buys retention WINDOW at constant RAM
37733
- * rather than giving RAM back — lower this knob if RAM is what you wanted.
37734
- * `0` DISABLES the lease entirely and falls the worker back to the tiny
37735
- * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
37736
- * to replace).
37737
- */
37738
- budgetMb: z.number().int().min(0).max(4096),
37739
- /**
37740
- * Demand window: eager per-frame native retention runs only within this many
37741
- * ms of the last native-crop request (or of the dial starting).
37742
- *
37743
- * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
37744
- * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
37745
- * per second on a camera with zero crop demand), so leave it non-zero unless
37746
- * you are reproducing that.
37747
- */
37748
- activityMs: z.number().int().min(0).max(12e4),
37749
- /**
37750
- * Which delivered frames are retained at all — see
37751
- * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
37752
- * changes WHAT is kept rather than for how long, so it is also the only one
37753
- * that can turn a crop that used to hit into a miss. The worker counts every
37754
- * crop request naming a frame it did NOT see marked
37755
- * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
37756
- * there is the signal that some caller names frames outside the inference set
37757
- * and that this must go back to `all`.
37758
- */
37759
- admission: NativeLeaseAdmissionSchema
37760
- });
37761
- /**
37762
- * The values in force when the operator has set nothing — byte-for-byte the
37763
- * constants the decode worker shipped with as env-var defaults, so making these
37764
- * settings changed no behaviour on the day it landed.
37765
- */
37766
- var DEFAULT_NATIVE_LEASE_SETTINGS = {
37767
- ttlMs: 1200,
37768
- budgetMb: 1024,
37769
- activityMs: 15e3,
37770
- admission: "inferred"
37771
- };
37772
- /** Slider bounds for the operator-facing knobs (orchestrator settings UI). */
37773
- var NATIVE_LEASE_TTL_FIELD = {
37774
- min: 250,
37775
- max: 1e4,
37776
- step: 50,
37777
- default: DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs
37778
- };
37779
- var NATIVE_LEASE_BUDGET_FIELD = {
37780
- min: 0,
37781
- max: 4096,
37782
- step: 64,
37783
- default: DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb
37784
- };
37785
- var NATIVE_LEASE_ACTIVITY_FIELD = {
37786
- min: 0,
37787
- max: 12e4,
37788
- step: 1e3,
37789
- default: DEFAULT_NATIVE_LEASE_SETTINGS.activityMs
37790
- };
37791
- /** Select options for the admission knob (orchestrator settings UI). */
37792
- var NATIVE_LEASE_ADMISSION_FIELD = {
37793
- options: [{
37794
- value: "all",
37795
- label: "Every delivered frame"
37796
- }, {
37797
- value: "inferred",
37798
- label: "Only frames sent to inference"
37799
- }],
37800
- default: DEFAULT_NATIVE_LEASE_SETTINGS.admission
37801
- };
37802
- /**
37803
- * Parse one knob, reporting `null` for absent, junk, out-of-bounds — AND for a
37804
- * value equal to the shipped default.
37805
- *
37806
- * That last rule is not tidiness, it is the difference between the documented
37807
- * precedence being true and being a lie. `addon-settings.getGlobalSettings`
37808
- * returns a HYDRATED payload, and `hydrateField` fills an unstored field with
37809
- * the schema's own `default` (verified live on the hub: a cluster that has never
37810
- * opened the form still reports `nativeLeaseTtlMs = 1200`). A reader that took
37811
- * that at face value would report all three knobs as "set" on every cluster on
37812
- * the day this shipped, permanently retiring the `CAMSTACK_SESSION_NATIVE_LEASE_*`
37813
- * emergency override that the precedence promises. There is no raw-store read on
37814
- * this cap to distinguish the two, so the default value itself is treated as
37815
- * "the operator has expressed no preference" — which is also what leaving a
37816
- * slider untouched means.
37817
- *
37818
- * The cost is one honest edge: an operator who deliberately selects the default
37819
- * value in order to overrule an env var does not get it. Clear the env var
37820
- * instead; the worker's spawn line names the source, so this is visible rather
37821
- * than mysterious.
37822
- */
37823
- function readKnob(knob, raw) {
37824
- const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(raw);
37825
- if (!parsed.success) return null;
37826
- return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS[knob] ? null : parsed.data;
37827
- }
37828
- /** {@link readKnob} for the one non-numeric knob. Same default-means-unset rule. */
37829
- function readAdmissionKnob(raw) {
37830
- const parsed = NativeLeaseAdmissionSchema.safeParse(raw);
37831
- if (!parsed.success) return null;
37832
- return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS.admission ? null : parsed.data;
37833
- }
37834
- /**
37835
- * Narrow a FLAT settings record to the knobs the operator set.
37836
- *
37837
- * Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
37838
- * budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
37839
- * clamped, not defaulted) so the caller can still fall through to the env
37840
- * override — clamping here would turn a typo into a value nobody chose. See
37841
- * {@link readKnob} for why the default counts as unset.
37842
- */
37843
- function readNativeLeaseOverride(config) {
37844
- const ttlMs = readKnob("ttlMs", config[NATIVE_LEASE_TTL_KEY]);
37845
- const budgetMb = readKnob("budgetMb", config[NATIVE_LEASE_BUDGET_KEY]);
37846
- const activityMs = readKnob("activityMs", config[NATIVE_LEASE_ACTIVITY_KEY]);
37847
- const admission = readAdmissionKnob(config[NATIVE_LEASE_ADMISSION_KEY]);
37848
- return {
37849
- ...ttlMs === null ? {} : { ttlMs },
37850
- ...budgetMb === null ? {} : { budgetMb },
37851
- ...activityMs === null ? {} : { activityMs },
37852
- ...admission === null ? {} : { admission }
37853
- };
37854
- }
37855
- function isHydratedField(entry) {
37856
- return typeof entry === "object" && entry !== null && "key" in entry;
37857
- }
37858
- var LEASE_KEYS = [
37859
- NATIVE_LEASE_TTL_KEY,
37860
- NATIVE_LEASE_BUDGET_KEY,
37861
- NATIVE_LEASE_ACTIVITY_KEY,
37862
- NATIVE_LEASE_ADMISSION_KEY
37863
- ];
37864
- /**
37865
- * Extract the operator's lease overrides from an
37866
- * `addon-settings.getGlobalSettings` payload.
37867
- *
37868
- * Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
37869
- * alone: the keys are unique across the addon's schema, and a section rename
37870
- * must not silently revert the whole cluster to the defaults. A `null` payload
37871
- * (addon mid-boot) means "operator set nothing" — the env/default fallback then
37872
- * applies, which is the correct read of "I could not ask".
37873
- */
37874
- function pickNativeLeaseOverride(view) {
37875
- if (view === null) return {};
37876
- const flat = {};
37877
- for (const section of view.sections) for (const entry of section.fields) {
37878
- if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
37879
- if (LEASE_KEYS.includes(entry.key)) flat[entry.key] = entry.value;
37880
- }
37881
- return readNativeLeaseOverride(flat);
37882
- }
37883
- //#endregion
37884
38176
  //#region src/helpers/bind-addon-actions.ts
37885
38177
  /**
37886
38178
  * Bind an addon's custom-action catalog to its tRPC surface, returning a
@@ -38132,4 +38424,4 @@ function enumerateInferenceDevices(hw) {
38132
38424
  return out;
38133
38425
  }
38134
38426
  //#endregion
38135
- export { ACCESSORY_LABEL, ALEXA_EGRESS_PROFILE, ALL_CAPABILITY_DEFINITIONS, APPLE_SA_TO_MACRO, AUDIO_ANALYSIS_CAP_NAME, AUDIO_BACKEND_CHOICES, AUDIO_MACRO_LABELS, AUDIO_PRESETS, AccessoriesStatusSchema, AccessoryKind, AddBrokerInputSchema, AddonAutoUpdateSchema, AddonListItemSchema, AddonPageDeclarationSchema, AddonPageInfoSchema, AdoptInputSchema as AdoptionAdoptInputSchema, AdoptResultSchema as AdoptionAdoptResultSchema, AdoptionFilterSchema, GetCandidateInputSchema as AdoptionGetCandidateInputSchema, ListCandidatesInputSchema as AdoptionListCandidatesInputSchema, ListCandidatesOutputSchema as AdoptionListCandidatesOutputSchema, ReleaseInputSchema as AdoptionReleaseInputSchema, AdoptionStatusSchema, AgentLoadSummarySchema, AirQualitySensorStatusSchema, AlarmArmModeSchema, AlarmPanelStatusSchema, AlarmStateSchema, AlertSchema, AlertSeveritySchema, AlertSourceSchema, AlertStatusSchema, AmbientLightSensorStatusSchema, ApiKeyRecordSchema, ApiKeySummarySchema, ArchiveEntrySchema, ArchiveManifestSchema, AttachmentMediaTypeSchema, AttachmentSchema, AudioAnalysisResultSchema, AudioAnalysisSettingsSchema, AudioChunkInputSchema, AudioClassSummarySchema, AudioClassificationLabelSchema, AudioClassificationResultSchema, AudioCodecInfoSchema, AudioDecodeSessionConfigSchema, AudioEncodeSchema, AudioEncodeSessionConfigSchema, AudioEncodedChunkSchema, AudioEventSchema, AudioLevelSchema, AudioMetricsHistoryPointSchema, AudioMetricsHistorySchema, AudioMetricsSnapshotSchema, AudioPcmChunkSchema, AuthResultSchema, AutoUpdateSettingsSchema, AutomationControlStatusSchema, AvailableIntegrationTypeSchema, BACKEND_TO_FORMAT, BASE_LIVE_EGRESS_PROFILE, BATTERY_DEVICE_PROFILE, BacklightModeSchema, BackupDestinationInfoSchema, BackupEntrySchema, BaseAddon, BaseDevice, BaseDeviceProvider, BatteryStatusSchema, BinaryStatusSchema, BoundingBoxSchema, BrightnessStatusSchema, AddInputSchema as BrokerAddInputSchema, BrokerAudioClientSchema, BrokerClientsSchema, BrokerConnectionDetailsSchema, BrokerConsumerAttributionSchema, BrokerConsumerKindSchema, BrokerDecodedClientSchema, BrokerEncodedClientSchema, GetStateInputSchema as BrokerGetStateInputSchema, BrokerInfoSchema, BrokerProviderInfoSchema, PublishInputSchema as BrokerPublishInputSchema, RegistryStatusSchema as BrokerRegistryStatusSchema, BrokerRtspClientSchema, BrokerStatsSchema, BrokerStatusEnum, BrokerStatusSchema, SubscribeInputSchema as BrokerSubscribeInputSchema, SubscribeResultSchema as BrokerSubscribeResultSchema, TestConnectionResultSchema as BrokerTestConnectionResultSchema, UnsubscribeInputSchema as BrokerUnsubscribeInputSchema, CAMERA_SWITCH_CATALOG, CAMERA_SWITCH_ORDER, CAM_PROFILE_ORDER, CAPABILITY_NAMES, CAPABILITY_ROUTER_KEYS, CAP_NAMES_WITH_STATUS, CAP_NODE_PIN_CONTEXT_KEY, CAP_PROVIDER_KIND_MAP, COCO_80_LABELS, COCO_TO_MACRO, CamProfileSchema, CamStreamDescriptorSchema, CamStreamKindSchema, CamStreamResolutionSchema, CameraAssignmentStatusSchema, CameraAudioStatusSchema, CameraBrokerProfileSchema, CameraBrokerStatusSchema, CameraCredentialsSchema, CameraCredentialsStatusSchema, CameraDecoderShmSchema, CameraDecoderStatusSchema, CameraDetectionPhaseSchema, CameraDetectionProvisioningSchema, CameraDetectionProvisioningStateSchema, CameraDetectionStatusSchema, CameraMetricsSchema, CameraMetricsWithDeviceIdSchema, CameraMotionStatusSchema, CameraRecordingModeSchema, CameraRecordingStatusSchema, CameraSourceStatusSchema, CameraSourceStreamSchema, CameraStatusSchema, CameraStreamSchema, CameraSwitchAuthoritySchema, CameraSwitchGroupSchema, CameraSwitchIdSchema, CameraSwitchSchema, CameraSwitchUnavailableReasonSchema, CandidateQueryFilterSchema, CapScopeSchema, CapabilityBindingsSchema, CarbonMonoxideStatusSchema, ChargingStatus, ClientNetworkStatsSchema, ClimateControlStatusSchema, ClipPlaybackSchema, ClipSchema, ClusterAddonNodeDeploymentSchema, ClusterAddonStatusEntrySchema, CollectionColumnSchema, CollectionIndexSchema, ColorStatusSchema, ConfigEntrySchema, ConfigSectionWithValuesSchema, ConfigTabDeclarationSchema, ConnectivityStatusSchema, ConsumableItemSchema, ConsumablesStatusSchema, ContactStatusSchema, ControlKindSchema, ControlStatusSchema, ConvertArtifactSchema, ConvertResultSchema, ConvertTargetSchema, CoreBlockCompileResultSchema, CoreBlockInputSchema, CoreBlockPlacementSchema, CoreBlockSchema, CoreBlockStatusSchema, CoverStateSchema, CoverStatusSchema, CreateApiKeyInputSchema, CreateApiKeyResultSchema, CreateIntegrationInputSchema, CreateScopedTokenInputSchema, CreateScopedTokenResultSchema, CreateUserInputSchema, CustomActionInputSchema, CustomModelDescriptorSchema, DATAPLANE_SECRET_HEADER, DEFAULT_ADDON_PLACEMENT, DEFAULT_AUDIO_ANALYZER_CONFIG, DEFAULT_DECODER_HWACCEL_CONFIG, DEFAULT_DETAIL_CROP_CONVENTION, DEFAULT_EVENT_COLOR, DEFAULT_FEATURES, DEFAULT_NATIVE_LEASE_SETTINGS, DEFAULT_RETENTION, DEFAULT_SCRUB_THUMBNAIL_PRESET, DETAIL_CROP_PADDING_FIELD, DETAIL_CROP_PADDING_KEY, DETAIL_CROP_SECTION_ID, DETAIL_CROP_SQUARE_KEY, DETECTION_PIPELINE_CAP_NAME, DEVICE_BACKEND_TO_FORMAT, DEVICE_CAP_NAMES, DEVICE_PROFILES, DEVICE_SCOPED_CAPS, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATE_READERS, DEVICE_STATUS_METHOD, DEVICE_TYPE_CONTROL_KIND, DEVICE_TYPE_INFO, EngineInfoSchema as DataStoreEngineInfoSchema, DayNightModeSchema, DayNightOptionsSchema, DayNightSettingsPatchSchema, DayNightStatusSchema, DecodedAudioChunkSchema, DecodedFrameSchema, DecoderSessionConfigSchema, DecoderStatsSchema, DeleteIntegrationResultSchema, DetailCropConventionSchema, DetectionSourceSchema, DeviceCodeSeveritySchema, DeviceConfig, DeviceDiscoveryStatusSchema, ExposeInputSchema as DeviceExportExposeInputSchema, DeviceExportStatusSchema, UnexposeInputSchema as DeviceExportUnexposeInputSchema, DeviceFeature, DeviceInfoSchema, DeviceLinkModeSchema, DeviceNetworkStatsSchema, DeviceRole, DeviceRuntimeState, DeviceStatusSchema, DeviceType, DiscoveredChildDeviceSchema, DiscoveredChildStatusSchema, DiscoveredDeviceSchema, DiscoveredTargetSchema, DisposerChain, DoorbellPressEventSchema, DoorbellStatusSchema, EVENTFUL_CAP_NAMES, EVENT_KIND_BY_CAP, EVENT_PAD_MS, EVENT_TAXONOMY, EXPRESSION_BUILTINS, EXPRESSION_BUILTIN_NAMES, EXPRESSION_COMPILE_CACHE_CAPACITY, EXPRESSION_IDENTIFIER_RE, EXPRESSION_INJECTED_NOW, EgressEncodeSchema, EgressRateControlSchema, EgressTranscodeRequestSchema, EgressTranscodeSchema, ElementConfigStore, EmbeddingInfoSchema, EmbeddingResultSchema, EncodeProfileSchema, EncodedPacketSchema, EnrichedWidgetMetadataSchema, EnumSensorDateTimeFormatSchema, EnumSensorStatusSchema, EventCategory, EventEmitterStatusSchema, EventFireSchema, EventItemSchema, EventKindCategorySchema, EventKindDescriptorSchema, EventKindIconSchema, EventKindSchema, EventKindsForDeviceSchema, EventMediaArtifactSchema, EventMediaCoverageSchema, EventMediaKindSchema, EventMediaProductionSchema, EventSourceType, ExportDownloadSchema, ExportOptionsSchema, ExportRecordSchema, ExportSetupFieldSchema, ExportSetupSchema, ExportSpeedSchema, ExportStateSchema, ExportTimelapseSchema, ExposedDeviceSchema, ExposureModeSchema, ExpressionEvalError, ExpressionParseError, FanControlStatusSchema, FanDirectionSchema, FeatureManifestSchema, FeatureProbeStatusSchema, FloodStatusSchema, Fmp4BoxSplitter, FrameHandleFormatSchema, FrameHandleSchema, FrameInputSchema, GasStatusSchema, GetStreamWithCodecInputSchema, GlobalMetricsSchema, HAP_AUDIO_BASE, HAP_AUDIO_BITRATE_KBPS, HAP_AUDIO_VBV_KBITS, HAP_KEYFRAME_INTERVAL_SEC, HF_BASE_URL, HF_REPO, HWACCEL_OPTIONS, HealthStatusSchema, HistoryPointSchema, HistoryResolutionEnum, HumidifierStatusSchema, HumiditySensorStatusSchema, HvacModeSchema, ImageContractSchema, ImageContractStateSchema, ImageRotateSchema, ImageSettingsOptionsSchema, ImageSettingsPatchSchema, ImageSettingsStatusSchema, ImageStatusSchema, IngestOwnerSchema, InstalledPackageSchema, IntegrationLiteSchema, IntegrationWithStateSchema, IntercomAbilitySchema, IntercomStatusSchema, KNOWN_CAP_NAMES, KeyEventSchema, LOG_LEVEL_RANK, LabelAttributionSchema, LabelDefinitionSchema, LabelTierSchema, LawnMowerActivitySchema, LawnMowerControlStatusSchema, LinkedDeviceSchema, LlmDefaultSchema, LlmDefaultSelectorSchema, LlmErrorCodeSchema, LlmGenerateBaseInputSchema, LlmGenerateErrSchema, LlmGenerateOkSchema, LlmGenerateResultSchema, LlmImageSchema, LlmNodeModelSchema, LlmProfileKindDescriptorSchema, LlmProfileKindSchema, LlmProfileSchema, LlmRuntimeCompleteInputSchema, LlmRuntimeDiskUsageSchema, LlmRuntimeNodeSchema, LlmRuntimeStatusSchema, LlmUsageRollupSchema, LlmUsageSchema, LocateSegmentResultSchema, LocationStatSchema, LockControlStatusSchema, LockStateSchema, LogEntrySchema, LogLevelSchema, LogStreamEntrySchema, LoginMethodContributionSchema, LoginStageEnum, MACRO_LABELS, MAX_EXPRESSION_AST_NODES, MAX_EXPRESSION_BINDINGS, MAX_EXPRESSION_CALL_ARGS, MAX_EXPRESSION_EVAL_STEPS, MAX_EXPRESSION_SOURCE_LENGTH, METHOD_ACCESS_MAP, MODEL_FORMATS, MOTION_TRIGGER_FEATURE, ManagedModelCatalogEntrySchema, ManagedModelRefSchema, ManagedRuntimeConfigSchema, MaskGridDimsSchema, MaskGridShapeSchema, MaskLineShapeSchema, MaskPointSchema, MaskPolygonShapeSchema, MaskPolygonVerticesSchema, MaskRectShapeSchema, MaskShapeKindSchema, MaskShapeSchema, MediaFileInfoSchema, MediaFileSchema, MediaPlayerRepeatSchema, MediaPlayerStateSchema, MediaPlayerStatusSchema, MeshPeerSchema, MeshStatusSchema, MethodAccessSchema, ModelCatalogEntrySchema, ModelConvertInputSchema, ModelConvertMetadataSchema, ModelDistributeInputSchema, ModelDistributeResultSchema, ModelExtraFileSchema, ModelFormatEntrySchema, ModelFormatsSchema, ModelSubstitutionSchema, ModelVariantGroupSchema, MotionAnalysisResultSchema, MotionEventSchema, MotionOnMotionChangedDataSchema, MotionRegionSchema, MotionSourceEnum, MotionSourcesSchema, MotionStatusSchema, MotionTriggerRuntimeStateSchema, MotionTriggerStatusSchema, MotionZoneOptionsSchema, MotionZonePatchSchema, MotionZoneRegionSchema, MotionZoneStatusSchema, StatusSchema as MqttBrokerStatusSchema, MutationFilterSchema, NATIVE_LEASE_ACTIVITY_FIELD, NATIVE_LEASE_ACTIVITY_KEY, NATIVE_LEASE_ADMISSION_FIELD, NATIVE_LEASE_ADMISSION_KEY, NATIVE_LEASE_BUDGET_FIELD, NATIVE_LEASE_BUDGET_KEY, NATIVE_LEASE_SECTION_ID, NATIVE_LEASE_TTL_FIELD, NATIVE_LEASE_TTL_KEY, NC_BASE_CONDITION_KEYS, NC_CONDITION_CATALOG, NC_HISTORY_LIMIT_DEFAULT, NC_HISTORY_LIMIT_MAX, NC_MAX_PER_TRACK_IMMEDIATE, NC_SNOOZE_MAX_MINUTES, NC_TAXONOMY, NativeCropBboxSchema, NativeCropRefSchema, NativeCropResultSchema, NativeDetectionSchema, NativeLeaseAdmissionSchema, NativeLeaseSettingsSchema, NativeObjectClassEnum, NativeObjectDetectionRuntimeStateSchema, NativeObjectDetectionStatusSchema, NcAlarmConfigSchema, NcAlarmModeCoverageSchema, NcAlarmSettingsPatchSchema, NcAlarmSettingsSchema, NcConditionDescriptorSchema, NcConditionsSchema, NcCrossingSchema, NcDeliverySchema, NcDeviceStateConditionSchema, NcHistoryEntrySchema, NcHistoryFilterSchema, NcHistoryRecordKindSchema, NcHistoryStatusSchema, NcHistorySubjectSchema, NcMediaFrameSchema, NcMediaPolicySchema, NcOccupancyConditionSchema, NcPlateMatcherSchema, NcRuleActionSchema, NcRuleActionSequenceSchema, NcRuleActionsSchema, NcRuleInputSchema, NcRuleNotificationButtonSchema, NcRulePatchSchema, NcRuleSchema, NcRuleTargetSchema, NcScheduleSchema, NcScheduleWindowSchema, NcSnoozeInputSchema, NcSnoozeSchema, NcSnoozeScopeSchema, NcSnoozeSuppressedSchema, NcTaxonomyEntrySchema, NcTaxonomySchema, NcTestResultSchema, NcThrottleGranularitySchema, NcThrottleSchema, NcZoneConditionSchema, NetworkAccessStatusSchema, NetworkAddressSchema, NetworkEndpointSchema, NotificationActionIconSchema, NotificationActionSchema, NotificationFormatSchema, NotificationSchema, NotifierStatusSchema, NumericSensorStatusSchema, OPS_LOG_DEFAULT_LIMIT, OPS_LOG_RING_DEFAULT_MAX, OauthIntegrationDescriptorSchema, ObjectEventSchema, OpsLogDomainSchema, OpsLogEntrySchema, OpsLogOpSchema, OpsLogQueryInputSchema, OpsLogReasonSchema, OrchestratorMetricsSchema, OsdOverlayKindEnum, OsdOverlayPatchSchema, OsdOverlaySchema, OsdPositionEnum, OsdRenderOutcomeEnum, OsdRenderResultSchema, OsdSlotBindingSchema, OsdSlotViewSchema, OsdSourceOptionSchema, OsdSourceSchema, OsdSourceValueTypeEnum, OsdStatusSchema, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, PIPELINE_FLOW_CAPABILITY_NAMES, PIPELINE_OWNER_CAPABILITY_NAMES, PRIVACY_MASK_CAP_NAME, PROVIDER_KIND_CAP_NAMES, PYTHON_SCRIPT, PackageUpdateSchema, PackageVersionInfoSchema, PasskeyLoginMethodSchema, PasskeySummarySchema, PcmSampleFormatSchema, PerScopeBreakdownSchema, PetFeederStatusSchema, PickStreamPreferencesSchema, PickStreamRequirementsSchema, PickedCamStreamSchema, PipelineAssignmentSchema, PipelineDefaultStepSchema, PipelineEngineChoiceSchema, PipelineRunResultBridge, PipelineStepInputSchema, PipelineValidationIssueSchema, PipelineValidationResultSchema, PlaceholderReasonSchema, PolygonPointSchema, PowerMeterStatusSchema, PresenceStatusSchema, PressureSensorStatusSchema, PrivacyMaskOptionsSchema, PrivacyMaskPatchSchema, PrivacyMaskRegionSchema, PrivacyMaskShapeSchema, PrivacyMaskStatusSchema, ProfileRtspEntrySchema, ProfileSlotSchema, ProfileSlotStatusSchema, ProviderStatusSchema, PtzAutotrackRuntimeStateSchema, PtzAutotrackSettingsSchema, PtzAutotrackStatusSchema, PtzAutotrackTargetOptionSchema, PtzMoveCommandSchema, PtzOptionsSchema, PtzPositionSchema, PtzPresetSchema, PtzStatusSchema, QueryFilterSchema, RATE_CONTROL_RELAXED, RATE_CONTROL_TIGHT, REACHABILITY_FAILURES_TO_OFFLINE, REACHABILITY_POLL_INTERVAL_MS, REACHABILITY_PROBE_TIMEOUT_MS, RECOGNITION_TYPES, RESERVED_BINDING_NAMES, RUNTIME_DEFAULTS, RUNTIME_TO_FORMAT, RawStateResultSchema, ReadGopBytesResultSchema, ReadSegmentBytesResultSchema, ReadinessRegistry, ReadinessTimeoutError, RecentTracksPageSchema, RecentTracksQueryInput, RecordingAvailabilitySchema, RecordingBandModeSchema, RecordingBandSchema, RecordingBandTriggersSchema, RecordingConfigSchema, RecordingDaysSchema, RecordingDeviceUsageSchema, RecordingLocationUsageSchema, RecordingManifestSchema, RecordingRangeSchema, RecordingRetentionSchema, RecordingStatusSchema, RecordingStorageModeSchema, RecordingStorageUsageSchema, RecordingTriggersSchema, RecordingWeekdaySchema, RedirectLoginMethodSchema, RelocateFootageInputSchema, RelocateJobSchema, RelocateJobStateSchema, RelocateMediaInputSchema, RenderedAsSchema, ReportMotionInputSchema, RetrainAnnotationDraftSchema, RetrainAnnotationKindSchema, RetrainAnnotationSchema, RetrainAnnotationSourceSchema, RetrainAssistResultSchema, RetrainAssistSubjectSchema, RetrainCopyRefusalSchema, RetrainFrameCandidateSchema, RetrainFrameListSchema, RetrainFrameSchema, RetrainFrameSelectionSchema, RetrainMacroClassSchema, RetrainStatusSchema, RetrainTrackSchema, RetrainTransitionResultSchema, RingBuffer, RtpSourceSchema, RtspRestreamEntrySchema, RunnerCameraConfigSchema, RunnerCameraDeviceUIFields, RunnerFrameSourceSchema, RunnerInferenceDeviceSchema, RunnerLocalLoadSchema, RunnerLocalMetricsSchema, SCOPE_PRESETS, SCRUB_THUMBNAIL_PRESETS, SCRUB_THUMBNAIL_PRESET_LABELS, SCRUB_THUMBNAIL_PRESET_ORDER, SENSOR_FEATURES, SENSOR_MAP, SOURCE_INFO_METADATA_KEY, STREAM_PROFILE_META, STREAM_QUALITY_LABELS, SUB_DETECTION_TYPES, SYSTEM_CAP_NAMES, SceneCheckSchema, SceneConditionSchema, SceneMonitorSchema, SceneMonitorStateSchema, SceneMonitorStatusSchema, SceneReferenceSchema, ScopedTokenSchema, ScopedTokenSummarySchema, ScoredObjectEventSchema, ScriptRunnerStatusSchema, ScrubThumbnailPresetSchema, SearchResultSchema, SendEmailInputSchema, SendEmailResultSchema, SendResultSchema, SensorEventSchema, ServerBootModeSchema, ServerPackageStatusSchema, ServerRollbackInfoSchema, ServerUpdateActionResultSchema, ServerUpdateCheckResultSchema, ServerUpdateStateSchema, SettingsPatchSchema, SettingsRecordSchema, SettingsSchemaWithValuesSchema, SettingsUpdateResultSchema, ShmRingStatsSchema, SmokeStatusSchema, SmtpStatusSchema, SnapshotImageSchema, SourceInfoSchema, SpatialDetectionSchema, SsoBridgeClaimsSchema, StartEmbeddedInputSchema, StationaryObjectSchema, AbortUploadInputSchema as StorageAbortUploadInputSchema, BeginDownloadInputSchema as StorageBeginDownloadInputSchema, BeginDownloadResultSchema as StorageBeginDownloadResultSchema, BeginUploadInputSchema as StorageBeginUploadInputSchema, BeginUploadResultSchema as StorageBeginUploadResultSchema, EndDownloadInputSchema as StorageEndDownloadInputSchema, FinalizeUploadInputSchema as StorageFinalizeUploadInputSchema, StorageLocationDeclarationSchema, StorageLocationRefSchema, StorageLocationSchema, StorageLocationTypeSchema, ProviderInfoSchema as StorageProviderInfoSchema, ReadChunkInputSchema as StorageReadChunkInputSchema, TestLocationResultSchema as StorageTestLocationResultSchema, WriteChunkInputSchema as StorageWriteChunkInputSchema, StreamCodecSchema, StreamFormatSchema, StreamNetworkStatsSchema, StreamParamsOptionsSchema, StreamParamsStatusSchema, StreamProfileConfigSchema, StreamProfileOptionsSchema, StreamProfilePatchSchema, StreamProfileSchema, StreamSourceEntrySchema, StreamSourceSchema, SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, SubscribeFramesInputSchema, SubscribeFramesResultSchema, SwitchStatusSchema, SystemMetricsSchema, SystemMirror, TAXONOMY_COLORS, TIMEZONES, TRANSCODE_DOWN_MAX_BITRATE_KBPS, TRANSCODE_DOWN_MAX_HEIGHT, TamperStatusSchema, TankStatusSchema, TargetKindCapsSchema, TargetKindLevelSchema, TargetKindSchema, TargetSchema, TemperatureSensorStatusSchema, TerminalProfileInfoSchema, TerminalSessionInfoSchema, TestConnectionResultSchema$1 as TestConnectionResultSchema, TestResultSchema, TimelapseRuleInputSchema, TimelapseRulePatchSchema, TimelapseRuleSchema, TimelapseTemplateSchema, ToastSchema, TokenScopeSchema, TopologyNodeSchema, TopologyProcessSchema, TopologyServiceSchema, TrackCascadeCountsSchema, TrackEnvelopeSchema, TrackFlagsPatchSchema, TrackFlagsSchema, TrackProjectionSchema, TrackSchema, TrackSourceSchema, TrackStateSchema, TrackZoneFilterSchema, TrackedDetectionSchema, TrainingExportDeviceTotalsSchema, TrainingExportSummarySchema, TurnServerSchema, UNIT_TABLE, BrokerInfoSchema$1 as UnifiedBrokerInfoSchema, UnitConversionError, UpdateIntegrationInputSchema, UpdateStatusSchema, UpdateUserInputSchema, UserRecordSchema, UserSummarySchema, VacuumControlStatusSchema, VacuumStateSchema, ValveStateSchema, ValveStatusSchema, VectorDeclareIndexInputSchema, VectorDeleteByFilterInputSchema, VectorDeleteInputSchema, VectorDeleteResultSchema, VectorFilterSchema, VectorGetInputSchema, VectorGetResultSchema, VectorItemSchema, VectorMatchSchema, VectorMetadataSchema, VectorMetricSchema, VectorQueryInputSchema, VectorQueryResultSchema, VectorStatsInputSchema, VectorStatsResultSchema, VectorUpsertInputSchema, VectorUpsertResultSchema, VibrationStatusSchema, VideoEncodeSchema, WEBRTC_EGRESS_PROFILE, WELL_KNOWN_TABS, WELL_KNOWN_TAB_MAP, WaterHeaterStatusSchema, WeatherStatusSchema, WebrtcStreamChoiceSchema, WebrtcStreamTargetSchema, WhiteBalanceModeSchema, WidgetHostEnum, WidgetLoginMethodSchema, WidgetMetadataSchema, WidgetRemoteSchema, WidgetSizeEnum, YAMNET_TO_MACRO, ZoneCrossingDirectionSchema, ZoneCrossingSchema, ZoneKindEnum, ZoneRuleModeEnum, ZoneRuleSchema, ZoneRuleStageEnum, ZoneRulesArraySchema, ZoneSchema, ZoneScopeBreakdownSchema, accessoriesCapability, accessoryStableId, addonPagesCapability, addonPagesSourceCapability, addonRoutesCapability, addonSettingsCapability, addonWidgetsCapability, addonWidgetsSourceCapability, addonsCapability, adminUiCapability, airQualitySensorCapability, alarmPanelCapability, alertsCapability, ambientLightSensorCapability, applyTransform, asBoolean, asJsonArray, asJsonObject, asNumber, asString, audioAnalysisCapability, audioAnalyzerCapability, audioCodecCapability, audioMetricsCapability, audioPlanFromEncodeProfile, authProviderCapability, autoAssignProfiles, automationControlCapability, backupCapability, bareAddonId, batteryCapability, bestLocationMatch, binaryCapability, bindAddonActions, brightnessCapability, brokerCapability, buildAddonRouteProvider, buildAudioArgs, buildEventKindDescriptor, buildFfmpegArgs, buildInputArgs, buildModelVariantGroups, buildNcTaxonomy, buildStreamParamsConfigSchema, buildVideoArgs, buttonCapability, cameraCredentialsCapability, cameraPipelineConfigCapability, cameraStreamsCapability, canConvertUnit, canonicalEgressPlan, carbonMonoxideCapability, cellsToRects, classifyStream, classifyStreams, climateControlCapability, collectHydratedFieldEntries, collectHydratedFieldValues, colorCapability, colorForKind, compileExpression, compileExpressionSafe, connectivityCapability, consumablesCapability, contactCapability, controlCapability, convertUnit, coreBlocksCapability, cosineSimilarity, coverCapability, createDeviceProxy, createDurableState, createEvent, createExpressionScope, createHwAccelCache, createLazyTrpcSource, createMirrorSource, createRuntimeStateBridge, createSliceHandle, createSystemProxy, customAction, customModelRegistryCapability, dataStoreProviderCapability, dayNightCapability, decodeVectorBase64, decoderCapability, defaultDeviceFor, defineCustomActions, deriveCameraSwitches, deriveDetailCropRect, deriveRecordingMode, describeModelVariant, detectionPipelineCapability, deviceAdoptionCapability, deviceBackendToFormat, deviceCustomAction, deviceDiscoveryCapability, deviceExportCapability, deviceManagerCapability, deviceMatchesProfile, deviceOpsCapability, deviceProviderCapability, deviceStateCapability, deviceStatusCapability, doorbellCapability, egressTranscodeSharingKey, egressTransportFromRequest, embeddingEncoderCapability, emitDownForOwnedCaps, emitReadiness, encodeProfileFromStreamShape, encodeVectorBase64, enumSensorCapability, enumerateInferenceDevices, enumerateItemArrayFields, enumerateSchemaFields, errMsg, evaluateAst, evaluateLinkExpression, evaluateZoneRules, event, eventEmitterCapability, eventsCapability, expandCapMethods, extractNestedAddonId, extractSourceInfoFromMetadata, faceGalleryCapability, fanControlCapability, featureProbeCapability, filesystemBrowseCapability, findTimezone, floodCapability, formatForBackend, formatForRuntime, gasCapability, getAudioMacroClassIds, getByPath, getCapsByProviderKind, getTaxonomyEntry, hasMotionTrigger, hfModelUrl, htmlToText, humidifierCapability, humiditySensorCapability, hydrateSchema, imageCapability, imageSettingsCapability, integrationsCapability, intercomCapability, invocationFromEncodeProfile, isAgentOnlyPlacement, isArrayOutputSchema, isBaseConditionKey, isCollectionArrayMethod, isDeployableToAgent, isDeviceConfigCap, isDeviceScopedCap, isEvent, isNode, isObjectInput, isSameAddonId, isScheduleActive, isSoftwareDecode, isVoidInput, jobKindSchema, kebabToCamel, knownValues, lawnMowerControlCapability, lifecycleJobSchema, lifecycleJobScopeSchema, lifecycleJobStateSchema, lifecycleTaskSchema, llmCapability, llmRuntimeCapability, localNetworkCapability, locationSimilarity, lockControlCapability, logBannerArgs, logDestinationCapability, logLevelAtMost, loginMethodCapability, looseSchema, makeProfileBrokerId, makeSourceBrokerId, mapAudioLabelToMacro, markdownToHtmlLite, markdownToText, maskUrlCredentials, mediaPlayerCapability, mergeSourceInfo, meshNetworkCapability, method, metricsProviderCapability, modelConvertCapability, modelDistributorCapability, modelFormatForRuntime, motionCapability, motionDetectionCapability, motionTriggerCapability, motionZonesCapability, mqttBrokerCapability, nativeObjectDetectionCapability, networkAccessCapability, networkQualityCapability, nodePin, nodesCapability, normalizeAddonInitResult, normalizeUnit, notificationOutputCapability, notificationRulesCapability, notifierCapability, numericSensorCapability, oauthIntegrationCapability, objectInputDeclaresAddonId, osdCapability, osdManagerCapability, parseCameraStreamConfig, parseExpression, parseJsonArray, parseJsonObject, parseJsonUnknown, parseProfileBrokerId, parseStreamParamsFormPatch, petFeederCapability, pickAccessoryControl, pickDetailCropConvention, pickNativeLeaseOverride, pickPreferredRtspEntry, pickVideoEncoder, pickerForCondition, pipelineAnalyticsCapability, pipelineExecutorCapability, pipelineOrchestratorCapability, pipelineRunnerCapability, plateGalleryCapability, platformProbeCapability, powerMeterCapability, prepareNotification, presenceCapability, pressureSensorCapability, privacyMaskCapability, procedureAuthKey, ptzAutotrackCapability, ptzCapability, pythonScriptForBackend, readDetailCropConvention, readDeviceStateFrom, readNativeLeaseOverride, readNodePin, readinessKey, rebootCapability, recordingCapability, recordingExportCapability, rectsToCells, requiresPython, resolveAddonExecution, resolveAddonGroup, resolveAddonPlacement, resolveAddonRuntime, resolveCapMount, resolveDetectionRuntime, resolveDeviceControlKind, resolveDeviceProfile, resolveEgressDecodeHwAccel, resolveFormat, resolveHydratedFieldValue, resolveModelFormat, resolveMutate, resolveRunnerId, resolveScrubThumbnailGeometry, resolveVariantModelId, runInferenceStep, runtimeDevices, sceneMonitorCapability, scopeKey, scoreRuntimes, scriptRunnerCapability, selectAssignedProfileSlots, serverManagementCapability, setByPath, settingsStoreCapability, sleep, sleepCancellable, smokeCapability, smtpProviderCapability, snapshotCapability, ssoBridgeCapability, startReachabilityPoll, stateVocabularyFor, storageCapability, storageEvictableCapability, storageProviderCapability, streamBrokerCapability, streamCatalogCapability, streamParamsCapability, streamPixels, streamQualityLabel, subKindsOf, summarisePrivacyAudio, supportedRuntimes, switchCapability, switchedOffIds, synthesizeSourceInfo, systemCapability, tamperCapability, taskLogEntrySchema, taskPhaseSchema, taskTargetSchema, temperatureSensorCapability, terminalSessionCapability, textToHtml, toDeviceSummary, toExpressionValue, toNodeId, toStreamSourceEntry, toastCapability, tokenize, transcodeBody, tryConvertUnit, turnProviderCapability, unitDimension, unitsForDimension, updateCapability, userManagementCapability, userPasskeysCapability, vacuumControlCapability, validateExpressionSource, valveCapability, vectorDimFromBase64, vectorStoreCapability, vibrationCapability, videoclipsCapability, viewerUiCapability, waterHeaterCapability, weatherCapability, webrtcClientHintsSchema, webrtcSessionCapability, wiringAddonHealthSchema, wiringHealthSnapshotSchema, wiringNodeHealthSchema, wiringProbeKindSchema, wiringProbeResultSchema, zodEntriesToConfigUI, zoneAnalyticsCapability, zoneRulesCapability, zonesCapability };
38427
+ export { ACCESSORY_LABEL, ALEXA_EGRESS_PROFILE, ALL_CAPABILITY_DEFINITIONS, APPLE_SA_TO_MACRO, AUDIO_ANALYSIS_CAP_NAME, AUDIO_BACKEND_CHOICES, AUDIO_MACRO_LABELS, AUDIO_PRESETS, AccessoriesStatusSchema, AccessoryKind, AddBrokerInputSchema, AddonAutoUpdateSchema, AddonListItemSchema, AddonPageDeclarationSchema, AddonPageInfoSchema, AdoptInputSchema as AdoptionAdoptInputSchema, AdoptResultSchema as AdoptionAdoptResultSchema, AdoptionFilterSchema, GetCandidateInputSchema as AdoptionGetCandidateInputSchema, ListCandidatesInputSchema as AdoptionListCandidatesInputSchema, ListCandidatesOutputSchema as AdoptionListCandidatesOutputSchema, ReleaseInputSchema as AdoptionReleaseInputSchema, AdoptionStatusSchema, AgentLoadSummarySchema, AirQualitySensorStatusSchema, AlarmArmModeSchema, AlarmPanelStatusSchema, AlarmStateSchema, AlertSchema, AlertSeveritySchema, AlertSourceSchema, AlertStatusSchema, AmbientLightSensorStatusSchema, ApiKeyRecordSchema, ApiKeySummarySchema, ArchiveEntrySchema, ArchiveManifestSchema, AttachmentMediaTypeSchema, AttachmentSchema, AudioAnalysisResultSchema, AudioAnalysisSettingsSchema, AudioChunkInputSchema, AudioClassSummarySchema, AudioClassificationLabelSchema, AudioClassificationResultSchema, AudioCodecInfoSchema, AudioDecodeSessionConfigSchema, AudioEncodeSchema, AudioEncodeSessionConfigSchema, AudioEncodedChunkSchema, AudioEventSchema, AudioLevelSchema, AudioMetricsHistoryPointSchema, AudioMetricsHistorySchema, AudioMetricsSnapshotSchema, AudioPcmChunkSchema, AuthResultSchema, AutoUpdateSettingsSchema, AutomationControlStatusSchema, AvailableIntegrationTypeSchema, BACKEND_TO_FORMAT, BASE_LIVE_EGRESS_PROFILE, BATTERY_DEVICE_PROFILE, BacklightModeSchema, BackupDestinationInfoSchema, BackupEntrySchema, BaseAddon, BaseDevice, BaseDeviceProvider, BatteryStatusSchema, BinaryStatusSchema, BoundingBoxSchema, BrightnessStatusSchema, AddInputSchema as BrokerAddInputSchema, BrokerAudioClientSchema, BrokerClientsSchema, BrokerConnectionDetailsSchema, BrokerConsumerAttributionSchema, BrokerConsumerKindSchema, BrokerDecodedClientSchema, BrokerEncodedClientSchema, GetStateInputSchema as BrokerGetStateInputSchema, BrokerInfoSchema, BrokerProviderInfoSchema, PublishInputSchema as BrokerPublishInputSchema, RegistryStatusSchema as BrokerRegistryStatusSchema, BrokerRtspClientSchema, BrokerStatsSchema, BrokerStatusEnum, BrokerStatusSchema, SubscribeInputSchema as BrokerSubscribeInputSchema, SubscribeResultSchema as BrokerSubscribeResultSchema, TestConnectionResultSchema as BrokerTestConnectionResultSchema, UnsubscribeInputSchema as BrokerUnsubscribeInputSchema, CAMERA_SWITCH_CATALOG, CAMERA_SWITCH_ORDER, CAM_PROFILE_ORDER, CAPABILITY_NAMES, CAPABILITY_ROUTER_KEYS, CAP_NAMES_WITH_STATUS, CAP_NODE_PIN_CONTEXT_KEY, CAP_PROVIDER_KIND_MAP, COCO_80_LABELS, COCO_TO_MACRO, CORE_BLOCK_ADDON_PREFIX, CamProfileSchema, CamStreamDescriptorSchema, CamStreamKindSchema, CamStreamResolutionSchema, CameraAssignmentStatusSchema, CameraAudioStatusSchema, CameraBrokerProfileSchema, CameraBrokerStatusSchema, CameraCredentialsSchema, CameraCredentialsStatusSchema, CameraDecoderShmSchema, CameraDecoderStatusSchema, CameraDetectionPhaseSchema, CameraDetectionProvisioningSchema, CameraDetectionProvisioningStateSchema, CameraDetectionStatusSchema, CameraMetricsSchema, CameraMetricsWithDeviceIdSchema, CameraMotionStatusSchema, CameraRecordingModeSchema, CameraRecordingStatusSchema, CameraSourceStatusSchema, CameraSourceStreamSchema, CameraStatusSchema, CameraStreamSchema, CameraSwitchAuthoritySchema, CameraSwitchGroupSchema, CameraSwitchIdSchema, CameraSwitchSchema, CameraSwitchUnavailableReasonSchema, CandidateQueryFilterSchema, CapScopeSchema, CapabilityBindingsSchema, CarbonMonoxideStatusSchema, ChargingStatus, ClientNetworkStatsSchema, ClimateControlStatusSchema, ClipPlaybackSchema, ClipSchema, ClusterAddonNodeDeploymentSchema, ClusterAddonStatusEntrySchema, CollectionColumnSchema, CollectionIndexSchema, ColorStatusSchema, ConfigEntrySchema, ConfigSectionWithValuesSchema, ConfigTabDeclarationSchema, ConnectivityStatusSchema, ConsumableItemSchema, ConsumablesStatusSchema, ContactStatusSchema, ControlKindSchema, ControlStatusSchema, ConvertArtifactSchema, ConvertResultSchema, ConvertTargetSchema, CoreBlockCompileResultSchema, CoreBlockInputSchema, CoreBlockPlacementSchema, CoreBlockSchema, CoreBlockStatusSchema, CoverStateSchema, CoverStatusSchema, CreateApiKeyInputSchema, CreateApiKeyResultSchema, CreateIntegrationInputSchema, CreateScopedTokenInputSchema, CreateScopedTokenResultSchema, CreateUserInputSchema, CustomActionInputSchema, CustomModelDescriptorSchema, DATAPLANE_SECRET_HEADER, DECLARED_DEVICE_SWEEP_LIMIT, DECLARED_INTEGRATION_FIXED_KEY, DEFAULT_ADDON_PLACEMENT, DEFAULT_AUDIO_ANALYZER_CONFIG, DEFAULT_DECODER_HWACCEL_CONFIG, DEFAULT_DETAIL_CROP_CONVENTION, DEFAULT_EVENT_COLOR, DEFAULT_FEATURES, DEFAULT_NATIVE_LEASE_SETTINGS, DEFAULT_RETENTION, DEFAULT_SCRUB_THUMBNAIL_PRESET, DETAIL_CROP_PADDING_FIELD, DETAIL_CROP_PADDING_KEY, DETAIL_CROP_SECTION_ID, DETAIL_CROP_SQUARE_KEY, DETECTION_PIPELINE_CAP_NAME, DEVICE_BACKEND_TO_FORMAT, DEVICE_CAP_NAMES, DEVICE_PROFILES, DEVICE_SCOPED_CAPS, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATE_READERS, DEVICE_STATUS_METHOD, DEVICE_TYPE_CONTROL_KIND, DEVICE_TYPE_INFO, EngineInfoSchema as DataStoreEngineInfoSchema, DayNightModeSchema, DayNightOptionsSchema, DayNightSettingsPatchSchema, DayNightStatusSchema, DeclaredDevices, DecodedAudioChunkSchema, DecodedFrameSchema, DecoderSessionConfigSchema, DecoderStatsSchema, DeleteIntegrationResultSchema, DetailCropConventionSchema, DetectionSourceSchema, DeviceCodeSeveritySchema, DeviceConfig, DeviceDiscoveryStatusSchema, ExposeInputSchema as DeviceExportExposeInputSchema, DeviceExportStatusSchema, UnexposeInputSchema as DeviceExportUnexposeInputSchema, DeviceFeature, DeviceInfoSchema, DeviceNetworkStatsSchema, DeviceRole, DeviceRuntimeState, DeviceStatusSchema, DeviceType, DiscoveredChildDeviceSchema, DiscoveredChildStatusSchema, DiscoveredDeviceSchema, DiscoveredTargetSchema, DisposerChain, DoorbellPressEventSchema, DoorbellStatusSchema, EVENTFUL_CAP_NAMES, EVENT_KIND_BY_CAP, EVENT_PAD_MS, EVENT_TAXONOMY, EXPRESSION_BUILTINS, EXPRESSION_BUILTIN_NAMES, EXPRESSION_COMPILE_CACHE_CAPACITY, EXPRESSION_IDENTIFIER_RE, EXPRESSION_INJECTED_NOW, EgressEncodeSchema, EgressRateControlSchema, EgressTranscodeRequestSchema, EgressTranscodeSchema, ElementConfigStore, EmbeddingInfoSchema, EmbeddingResultSchema, EncodeProfileSchema, EncodedPacketSchema, EnrichedWidgetMetadataSchema, EnumSensorDateTimeFormatSchema, EnumSensorStatusSchema, EventCategory, EventEmitterStatusSchema, EventFireSchema, EventItemSchema, EventKindCategorySchema, EventKindDescriptorSchema, EventKindIconSchema, EventKindSchema, EventKindsForDeviceSchema, EventMediaArtifactSchema, EventMediaCoverageSchema, EventMediaKindSchema, EventMediaProductionSchema, EventSourceType, ExportDownloadSchema, ExportOptionsSchema, ExportRecordSchema, ExportSetupFieldSchema, ExportSetupSchema, ExportSpeedSchema, ExportStateSchema, ExportTimelapseSchema, ExposedDeviceSchema, ExposureModeSchema, ExpressionBindingSourceSchema, ExpressionEvalError, ExpressionFieldBindingSchema, ExpressionGlobalBindingSchema, ExpressionLiteralBindingSchema, ExpressionParseError, ExpressionSourceSchema, FanControlStatusSchema, FanDirectionSchema, FeatureManifestSchema, FeatureProbeStatusSchema, FloodStatusSchema, Fmp4BoxSplitter, FrameHandleFormatSchema, FrameHandleSchema, FrameInputSchema, GasStatusSchema, GetStreamWithCodecInputSchema, GlobalMetricsSchema, HAP_AUDIO_BASE, HAP_AUDIO_BITRATE_KBPS, HAP_AUDIO_VBV_KBITS, HAP_KEYFRAME_INTERVAL_SEC, HF_BASE_URL, HF_REPO, HWACCEL_OPTIONS, HealthStatusSchema, HistoryPointSchema, HistoryResolutionEnum, HumidifierStatusSchema, HumiditySensorStatusSchema, HvacModeSchema, ImageContractSchema, ImageContractStateSchema, ImageRotateSchema, ImageSettingsOptionsSchema, ImageSettingsPatchSchema, ImageSettingsStatusSchema, ImageStatusSchema, IngestOwnerSchema, InstalledPackageSchema, IntegrationLiteSchema, IntegrationWithStateSchema, IntercomAbilitySchema, IntercomStatusSchema, KNOWN_CAP_NAMES, KeyEventSchema, LOG_LEVEL_RANK, LabelAttributionSchema, LabelDefinitionSchema, LabelTierSchema, LawnMowerActivitySchema, LawnMowerControlStatusSchema, LinkedDeviceSchema, LinkedDevicesModeSchema, LlmDefaultSchema, LlmDefaultSelectorSchema, LlmErrorCodeSchema, LlmGenerateBaseInputSchema, LlmGenerateErrSchema, LlmGenerateOkSchema, LlmGenerateResultSchema, LlmImageSchema, LlmNodeModelSchema, LlmProfileKindDescriptorSchema, LlmProfileKindSchema, LlmProfileSchema, LlmRuntimeCompleteInputSchema, LlmRuntimeDiskUsageSchema, LlmRuntimeNodeSchema, LlmRuntimeStatusSchema, LlmUsageRollupSchema, LlmUsageSchema, LocateSegmentResultSchema, LocationStatSchema, LockControlStatusSchema, LockStateSchema, LogEntrySchema, LogLevelSchema, LogStreamEntrySchema, LoginMethodContributionSchema, LoginStageEnum, MACRO_LABELS, MAX_EXPRESSION_AST_NODES, MAX_EXPRESSION_BINDINGS, MAX_EXPRESSION_CALL_ARGS, MAX_EXPRESSION_EVAL_STEPS, MAX_EXPRESSION_SOURCE_LENGTH, METHOD_ACCESS_MAP, MODEL_FORMATS, MOTION_TRIGGER_FEATURE, ManagedModelCatalogEntrySchema, ManagedModelRefSchema, ManagedRuntimeConfigSchema, MaskGridDimsSchema, MaskGridShapeSchema, MaskLineShapeSchema, MaskPointSchema, MaskPolygonShapeSchema, MaskPolygonVerticesSchema, MaskRectShapeSchema, MaskShapeKindSchema, MaskShapeSchema, MediaFileInfoSchema, MediaFileSchema, MediaPlayerRepeatSchema, MediaPlayerStateSchema, MediaPlayerStatusSchema, MeshPeerSchema, MeshStatusSchema, MethodAccessSchema, ModelCatalogEntrySchema, ModelConvertInputSchema, ModelConvertMetadataSchema, ModelDistributeInputSchema, ModelDistributeResultSchema, ModelExtraFileSchema, ModelFormatEntrySchema, ModelFormatsSchema, ModelSubstitutionSchema, ModelVariantGroupSchema, MotionAnalysisResultSchema, MotionEventSchema, MotionOnMotionChangedDataSchema, MotionRegionSchema, MotionSourceEnum, MotionSourcesSchema, MotionStatusSchema, MotionTriggerRuntimeStateSchema, MotionTriggerStatusSchema, MotionZoneOptionsSchema, MotionZonePatchSchema, MotionZoneRegionSchema, MotionZoneStatusSchema, StatusSchema as MqttBrokerStatusSchema, MutationFilterSchema, NATIVE_LEASE_ACTIVITY_FIELD, NATIVE_LEASE_ACTIVITY_KEY, NATIVE_LEASE_ADMISSION_FIELD, NATIVE_LEASE_ADMISSION_KEY, NATIVE_LEASE_BUDGET_FIELD, NATIVE_LEASE_BUDGET_KEY, NATIVE_LEASE_SECTION_ID, NATIVE_LEASE_TTL_FIELD, NATIVE_LEASE_TTL_KEY, NC_BASE_CONDITION_KEYS, NC_CONDITION_CATALOG, NC_HISTORY_LIMIT_DEFAULT, NC_HISTORY_LIMIT_MAX, NC_MAX_PER_TRACK_IMMEDIATE, NC_SNOOZE_MAX_MINUTES, NC_TAXONOMY, NativeCropBboxSchema, NativeCropRefSchema, NativeCropResultSchema, NativeDetectionSchema, NativeLeaseAdmissionSchema, NativeLeaseSettingsSchema, NativeObjectClassEnum, NativeObjectDetectionRuntimeStateSchema, NativeObjectDetectionStatusSchema, NcAlarmConfigSchema, NcAlarmModeCoverageSchema, NcAlarmSettingsPatchSchema, NcAlarmSettingsSchema, NcConditionDescriptorSchema, NcConditionsSchema, NcCrossingSchema, NcDeliverySchema, NcDeviceStateConditionSchema, NcHistoryEntrySchema, NcHistoryFilterSchema, NcHistoryRecordKindSchema, NcHistoryStatusSchema, NcHistorySubjectSchema, NcMediaFrameSchema, NcMediaPolicySchema, NcOccupancyConditionSchema, NcPlateMatcherSchema, NcRuleActionSchema, NcRuleActionSequenceSchema, NcRuleActionsSchema, NcRuleInputSchema, NcRuleNotificationButtonSchema, NcRulePatchSchema, NcRuleSchema, NcRuleTargetSchema, NcScheduleSchema, NcScheduleWindowSchema, NcSnoozeInputSchema, NcSnoozeSchema, NcSnoozeScopeSchema, NcSnoozeSuppressedSchema, NcTaxonomyEntrySchema, NcTaxonomySchema, NcTestResultSchema, NcThrottleGranularitySchema, NcThrottleSchema, NcZoneConditionSchema, NetworkAccessStatusSchema, NetworkAddressSchema, NetworkEndpointSchema, NotificationActionIconSchema, NotificationActionSchema, NotificationFormatSchema, NotificationSchema, NotifierStatusSchema, NumericSensorStatusSchema, OPS_LOG_DEFAULT_LIMIT, OPS_LOG_RING_DEFAULT_MAX, OauthIntegrationDescriptorSchema, ObjectEventSchema, OpsLogDomainSchema, OpsLogEntrySchema, OpsLogOpSchema, OpsLogQueryInputSchema, OpsLogReasonSchema, OrchestratorMetricsSchema, OsdOverlayKindEnum, OsdOverlayPatchSchema, OsdOverlaySchema, OsdPositionEnum, OsdRenderOutcomeEnum, OsdRenderResultSchema, OsdSlotBindingSchema, OsdSlotViewSchema, OsdSourceOptionSchema, OsdSourceSchema, OsdSourceValueTypeEnum, OsdStatusSchema, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, PIPELINE_FLOW_CAPABILITY_NAMES, PIPELINE_OWNER_CAPABILITY_NAMES, PRIVACY_MASK_CAP_NAME, PROVIDER_KIND_CAP_NAMES, PYTHON_SCRIPT, PackageUpdateSchema, PackageVersionInfoSchema, PasskeyLoginMethodSchema, PasskeySummarySchema, PcmSampleFormatSchema, PerScopeBreakdownSchema, PetFeederStatusSchema, PickStreamPreferencesSchema, PickStreamRequirementsSchema, PickedCamStreamSchema, PipelineAssignmentSchema, PipelineDefaultStepSchema, PipelineEngineChoiceSchema, PipelineRunResultBridge, PipelineStepInputSchema, PipelineValidationIssueSchema, PipelineValidationResultSchema, PlaceholderReasonSchema, PolygonPointSchema, PowerMeterStatusSchema, PresenceStatusSchema, PressureSensorStatusSchema, PrivacyMaskOptionsSchema, PrivacyMaskPatchSchema, PrivacyMaskRegionSchema, PrivacyMaskShapeSchema, PrivacyMaskStatusSchema, ProfileRtspEntrySchema, ProfileSlotSchema, ProfileSlotStatusSchema, ProviderStatusSchema, PtzAutotrackRuntimeStateSchema, PtzAutotrackSettingsSchema, PtzAutotrackStatusSchema, PtzAutotrackTargetOptionSchema, PtzMoveCommandSchema, PtzOptionsSchema, PtzPositionSchema, PtzPresetSchema, PtzStatusSchema, QueryFilterSchema, RATE_CONTROL_RELAXED, RATE_CONTROL_TIGHT, REACHABILITY_FAILURES_TO_OFFLINE, REACHABILITY_POLL_INTERVAL_MS, REACHABILITY_PROBE_TIMEOUT_MS, RECOGNITION_TYPES, RESERVED_BINDING_NAMES, RUNTIME_DEFAULTS, RUNTIME_TO_FORMAT, RawStateResultSchema, ReadGopBytesResultSchema, ReadSegmentBytesResultSchema, ReadinessRegistry, ReadinessTimeoutError, RecentTracksPageSchema, RecentTracksQueryInput, RecordingAvailabilitySchema, RecordingBandModeSchema, RecordingBandSchema, RecordingBandTriggersSchema, RecordingConfigSchema, RecordingDaysSchema, RecordingDeviceUsageSchema, RecordingLocationUsageSchema, RecordingManifestSchema, RecordingRangeSchema, RecordingRetentionSchema, RecordingStatusSchema, RecordingStorageModeSchema, RecordingStorageUsageSchema, RecordingTriggersSchema, RecordingWeekdaySchema, RedirectLoginMethodSchema, RelocateFootageInputSchema, RelocateJobSchema, RelocateJobStateSchema, RelocateMediaInputSchema, RenderedAsSchema, ReportMotionInputSchema, RetrainAnnotationDraftSchema, RetrainAnnotationKindSchema, RetrainAnnotationSchema, RetrainAnnotationSourceSchema, RetrainAssistResultSchema, RetrainAssistSubjectSchema, RetrainCopyRefusalSchema, RetrainFrameCandidateSchema, RetrainFrameListSchema, RetrainFrameSchema, RetrainFrameSelectionSchema, RetrainMacroClassSchema, RetrainStatusSchema, RetrainTrackSchema, RetrainTransitionResultSchema, RingBuffer, RtpSourceSchema, RtspRestreamEntrySchema, RunnerCameraConfigSchema, RunnerCameraDeviceUIFields, RunnerFrameSourceSchema, RunnerInferenceDeviceSchema, RunnerLocalLoadSchema, RunnerLocalMetricsSchema, SCOPE_PRESETS, SCRUB_THUMBNAIL_PRESETS, SCRUB_THUMBNAIL_PRESET_LABELS, SCRUB_THUMBNAIL_PRESET_ORDER, SENSOR_FEATURES, SENSOR_MAP, SOURCE_INFO_METADATA_KEY, STREAM_PROFILE_META, STREAM_QUALITY_LABELS, SUB_DETECTION_TYPES, SYSTEM_CAP_NAMES, SceneCheckSchema, SceneConditionSchema, SceneMonitorSchema, SceneMonitorStateSchema, SceneMonitorStatusSchema, SceneReferenceSchema, ScopedTokenSchema, ScopedTokenSummarySchema, ScoredObjectEventSchema, ScriptRunnerStatusSchema, ScrubThumbnailPresetSchema, SearchResultSchema, SendEmailInputSchema, SendEmailResultSchema, SendResultSchema, SensorEventSchema, ServerBootModeSchema, ServerPackageStatusSchema, ServerRollbackInfoSchema, ServerUpdateActionResultSchema, ServerUpdateCheckResultSchema, ServerUpdateStateSchema, SettingsPatchSchema, SettingsRecordSchema, SettingsSchemaWithValuesSchema, SettingsUpdateResultSchema, ShmRingStatsSchema, SmokeStatusSchema, SmtpStatusSchema, SnapshotImageSchema, SourceInfoSchema, SpatialDetectionSchema, SsoBridgeClaimsSchema, StartEmbeddedInputSchema, StationaryObjectSchema, AbortUploadInputSchema as StorageAbortUploadInputSchema, BeginDownloadInputSchema as StorageBeginDownloadInputSchema, BeginDownloadResultSchema as StorageBeginDownloadResultSchema, BeginUploadInputSchema as StorageBeginUploadInputSchema, BeginUploadResultSchema as StorageBeginUploadResultSchema, EndDownloadInputSchema as StorageEndDownloadInputSchema, FinalizeUploadInputSchema as StorageFinalizeUploadInputSchema, StorageLocationDeclarationSchema, StorageLocationRefSchema, StorageLocationSchema, StorageLocationTypeSchema, ProviderInfoSchema as StorageProviderInfoSchema, ReadChunkInputSchema as StorageReadChunkInputSchema, TestLocationResultSchema as StorageTestLocationResultSchema, WriteChunkInputSchema as StorageWriteChunkInputSchema, StreamCodecSchema, StreamFormatSchema, StreamNetworkStatsSchema, StreamParamsOptionsSchema, StreamParamsStatusSchema, StreamProfileConfigSchema, StreamProfileOptionsSchema, StreamProfilePatchSchema, StreamProfileSchema, StreamSourceEntrySchema, StreamSourceSchema, SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, SubscribeFramesInputSchema, SubscribeFramesResultSchema, SwitchStatusSchema, SystemMetricsSchema, SystemMirror, TAXONOMY_COLORS, TIMEZONES, TRANSCODE_DOWN_MAX_BITRATE_KBPS, TRANSCODE_DOWN_MAX_HEIGHT, TamperStatusSchema, TankStatusSchema, TargetKindCapsSchema, TargetKindLevelSchema, TargetKindSchema, TargetSchema, TemperatureSensorStatusSchema, TerminalProfileInfoSchema, TerminalSessionInfoSchema, TestConnectionResultSchema$1 as TestConnectionResultSchema, TestResultSchema, TimelapseRuleInputSchema, TimelapseRulePatchSchema, TimelapseRuleSchema, TimelapseTemplateSchema, ToastSchema, TokenScopeSchema, TopologyNodeSchema, TopologyProcessSchema, TopologyServiceSchema, TrackCascadeCountsSchema, TrackEnvelopeSchema, TrackFlagsPatchSchema, TrackFlagsSchema, TrackProjectionSchema, TrackSchema, TrackSourceSchema, TrackStateSchema, TrackZoneFilterSchema, TrackedDetectionSchema, TrainingExportDeviceTotalsSchema, TrainingExportSummarySchema, TurnServerSchema, UNIT_TABLE, BrokerInfoSchema$1 as UnifiedBrokerInfoSchema, UnitConversionError, UpdateIntegrationInputSchema, UpdateStatusSchema, UpdateUserInputSchema, UserRecordSchema, UserSummarySchema, VacuumControlStatusSchema, VacuumStateSchema, ValveStateSchema, ValveStatusSchema, VectorDeclareIndexInputSchema, VectorDeleteByFilterInputSchema, VectorDeleteInputSchema, VectorDeleteResultSchema, VectorFilterSchema, VectorGetInputSchema, VectorGetResultSchema, VectorItemSchema, VectorMatchSchema, VectorMetadataSchema, VectorMetricSchema, VectorQueryInputSchema, VectorQueryResultSchema, VectorStatsInputSchema, VectorStatsResultSchema, VectorUpsertInputSchema, VectorUpsertResultSchema, VibrationStatusSchema, VideoEncodeSchema, WEBRTC_EGRESS_PROFILE, WELL_KNOWN_TABS, WELL_KNOWN_TAB_MAP, WaterHeaterStatusSchema, WeatherStatusSchema, WebrtcStreamChoiceSchema, WebrtcStreamTargetSchema, WhiteBalanceModeSchema, WidgetHostEnum, WidgetLoginMethodSchema, WidgetMetadataSchema, WidgetRemoteSchema, WidgetSizeEnum, YAMNET_TO_MACRO, ZoneCrossingDirectionSchema, ZoneCrossingSchema, ZoneKindEnum, ZoneRuleModeEnum, ZoneRuleSchema, ZoneRuleStageEnum, ZoneRulesArraySchema, ZoneSchema, ZoneScopeBreakdownSchema, accessoriesCapability, accessoryStableId, addonPagesCapability, addonPagesSourceCapability, addonRoutesCapability, addonSettingsCapability, addonWidgetsCapability, addonWidgetsSourceCapability, addonsCapability, adminUiCapability, airQualitySensorCapability, alarmPanelCapability, alertsCapability, ambientLightSensorCapability, asBoolean, asJsonArray, asJsonObject, asNumber, asString, audioAnalysisCapability, audioAnalyzerCapability, audioCodecCapability, audioMetricsCapability, audioPlanFromEncodeProfile, authProviderCapability, autoAssignProfiles, automationControlCapability, backupCapability, bareAddonId, batteryCapability, bestLocationMatch, binaryCapability, bindAddonActions, brightnessCapability, brokerCapability, buildAddonRouteProvider, buildAudioArgs, buildEventKindDescriptor, buildFfmpegArgs, buildInputArgs, buildModelVariantGroups, buildNcTaxonomy, buildStreamParamsConfigSchema, buildVideoArgs, buttonCapability, cameraCredentialsCapability, cameraPipelineConfigCapability, cameraStreamsCapability, canConvertUnit, canonicalEgressPlan, carbonMonoxideCapability, cellsToRects, classifyStream, classifyStreams, climateControlCapability, collectHydratedFieldEntries, collectHydratedFieldValues, colorCapability, colorForKind, compileExpression, compileExpressionSafe, connectivityCapability, consumablesCapability, contactCapability, controlCapability, convertUnit, coreBlockAddonId, coreBlockIdFromAddonId, coreBlocksCapability, cosineSimilarity, coverCapability, createDeviceProxy, createDurableState, createEvent, createExpressionScope, createHwAccelCache, createLazyTrpcSource, createMirrorSource, createRuntimeStateBridge, createSliceHandle, createSystemProxy, customAction, customModelRegistryCapability, dataStoreProviderCapability, dayNightCapability, declarationOwnerNodeId, decodeVectorBase64, decoderCapability, defaultDeviceFor, defineCustomActions, deriveCameraSwitches, deriveDetailCropRect, deriveRecordingMode, describeModelVariant, detectionPipelineCapability, deviceAdoptionCapability, deviceBackendToFormat, deviceCustomAction, deviceDiscoveryCapability, deviceExportCapability, deviceManagerCapability, deviceMatchesProfile, deviceOpsCapability, deviceProviderCapability, deviceStateCapability, deviceStatusCapability, doorbellCapability, egressTranscodeSharingKey, egressTransportFromRequest, embeddingEncoderCapability, emitDownForOwnedCaps, emitReadiness, encodeProfileFromStreamShape, encodeVectorBase64, enumSensorCapability, enumerateInferenceDevices, enumerateItemArrayFields, enumerateSchemaFields, errMsg, evaluateAst, evaluateExpressionSource, evaluateZoneRules, event, eventEmitterCapability, eventsCapability, expandCapMethods, extractNestedAddonId, extractSourceInfoFromMetadata, faceGalleryCapability, fanControlCapability, featureProbeCapability, filesystemBrowseCapability, findTimezone, floodCapability, formatForBackend, formatForRuntime, gasCapability, getAudioMacroClassIds, getByPath, getCapsByProviderKind, getTaxonomyEntry, hasMotionTrigger, hfModelUrl, htmlToText, humidifierCapability, humiditySensorCapability, hydrateSchema, imageCapability, imageSettingsCapability, integrationsCapability, intercomCapability, invocationFromEncodeProfile, isAgentOnlyPlacement, isArrayOutputSchema, isBaseConditionKey, isCollectionArrayMethod, isDeployableToAgent, isDeviceConfigCap, isDeviceScopedCap, isEvent, isNode, isObjectInput, isSameAddonId, isScheduleActive, isSoftwareDecode, isVoidInput, jobKindSchema, kebabToCamel, knownValues, lawnMowerControlCapability, lifecycleJobSchema, lifecycleJobScopeSchema, lifecycleJobStateSchema, lifecycleTaskSchema, llmCapability, llmRuntimeCapability, localNetworkCapability, locationSimilarity, lockControlCapability, logBannerArgs, logDestinationCapability, logLevelAtMost, loginMethodCapability, looseSchema, makeProfileBrokerId, makeSourceBrokerId, mapAudioLabelToMacro, markdownToHtmlLite, markdownToText, maskUrlCredentials, mediaPlayerCapability, mergeSourceInfo, meshNetworkCapability, method, metricsProviderCapability, modelConvertCapability, modelDistributorCapability, modelFormatForRuntime, motionCapability, motionDetectionCapability, motionTriggerCapability, motionZonesCapability, mqttBrokerCapability, nativeObjectDetectionCapability, networkAccessCapability, networkQualityCapability, nodePin, nodesCapability, normalizeAddonInitResult, normalizeUnit, notificationOutputCapability, notificationRulesCapability, notifierCapability, numericSensorCapability, oauthIntegrationCapability, objectInputDeclaresAddonId, osdCapability, osdManagerCapability, parseCameraStreamConfig, parseExpression, parseJsonArray, parseJsonObject, parseJsonUnknown, parseProfileBrokerId, parseStreamParamsFormPatch, petFeederCapability, pickAccessoryControl, pickDetailCropConvention, pickNativeLeaseOverride, pickPreferredRtspEntry, pickVideoEncoder, pickerForCondition, pipelineAnalyticsCapability, pipelineExecutorCapability, pipelineOrchestratorCapability, pipelineRunnerCapability, plateGalleryCapability, platformProbeCapability, powerMeterCapability, prepareNotification, presenceCapability, pressureSensorCapability, privacyMaskCapability, procedureAuthKey, ptzAutotrackCapability, ptzCapability, pythonScriptForBackend, readDetailCropConvention, readDeviceStateFrom, readNativeLeaseOverride, readNodePin, readinessKey, rebootCapability, recordingCapability, recordingExportCapability, rectsToCells, requiresPython, resolveAddonExecution, resolveAddonGroup, resolveAddonPlacement, resolveAddonRuntime, resolveCapMount, resolveDetectionRuntime, resolveDeviceControlKind, resolveDeviceProfile, resolveEgressDecodeHwAccel, resolveFormat, resolveHydratedFieldValue, resolveModelFormat, resolveMutate, resolveRunnerId, resolveScrubThumbnailGeometry, resolveVariantModelId, runInferenceStep, runtimeDevices, sceneMonitorCapability, scopeKey, scoreRuntimes, scriptRunnerCapability, selectAssignedProfileSlots, serverManagementCapability, setByPath, settingsStoreCapability, sleep, sleepCancellable, smokeCapability, smtpProviderCapability, snapshotCapability, ssoBridgeCapability, startReachabilityPoll, stateVocabularyFor, storageCapability, storageEvictableCapability, storageProviderCapability, streamBrokerCapability, streamCatalogCapability, streamParamsCapability, streamPixels, streamQualityLabel, subKindsOf, summarisePrivacyAudio, supportedRuntimes, switchCapability, switchedOffIds, synthesizeSourceInfo, systemCapability, tamperCapability, taskLogEntrySchema, taskPhaseSchema, taskTargetSchema, temperatureSensorCapability, terminalSessionCapability, textToHtml, toDeviceSummary, toExpressionValue, toNodeId, toStreamSourceEntry, toastCapability, tokenize, transcodeBody, tryConvertUnit, turnProviderCapability, unitDimension, unitsForDimension, updateCapability, userManagementCapability, userPasskeysCapability, vacuumControlCapability, validateExpressionSource, valveCapability, vectorDimFromBase64, vectorStoreCapability, vibrationCapability, videoclipsCapability, viewerUiCapability, waterHeaterCapability, weatherCapability, webrtcClientHintsSchema, webrtcSessionCapability, wiringAddonHealthSchema, wiringHealthSnapshotSchema, wiringNodeHealthSchema, wiringProbeKindSchema, wiringProbeResultSchema, zodEntriesToConfigUI, zoneAnalyticsCapability, zoneRulesCapability, zonesCapability };