@camstack/system 1.2.61 → 1.2.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-package.d.ts +9 -27
- package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
- package/dist/builtins/device-manager/device-aggregation.d.ts +49 -2
- package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
- package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
- package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
- package/dist/builtins/device-manager/device-manager.addon.js +182 -888
- package/dist/builtins/device-manager/device-manager.addon.mjs +182 -888
- package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
- package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
- package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
- package/dist/builtins/device-manager/device-projection.d.ts +1 -2
- package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
- package/dist/builtins/device-manager/device-queries.d.ts +1 -1
- package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
- package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/homeassistant-integration.d.ts +7 -0
- package/dist/builtins/local-auth/index.d.ts +1 -0
- package/dist/builtins/local-auth/index.js +1 -0
- package/dist/builtins/local-auth/index.mjs +2 -2
- package/dist/builtins/local-auth/local-auth.addon.js +561 -497
- package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
- package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +319 -20
- package/dist/builtins/snapshot/index.mjs +319 -20
- package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
- package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +15 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +45 -5
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +45 -5
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-L6QPJWsr.mjs → dist-BJ2w3uoN.mjs} +1307 -1313
- package/dist/{dist-BN681PgO.js → dist-LDrOFrd-.js} +1255 -1273
- package/dist/index.d.ts +1 -1
- package/dist/index.js +209 -5
- package/dist/index.mjs +206 -7
- package/dist/kernel/addon-residue.d.ts +49 -0
- package/dist/kernel/index.d.ts +2 -0
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
- package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-CopL1_5C.mjs} +20 -14
- package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-DNfmBu20.js} +20 -14
- package/package.json +5 -1
- package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
- package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
- package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
- package/dist/main-CVLq8IzA.js +0 -2356
- package/dist/main-CzH19GkU.mjs +0 -2353
|
@@ -5434,6 +5434,69 @@ var StreamFormatSchema = z.enum([
|
|
|
5434
5434
|
"mjpeg",
|
|
5435
5435
|
"rtsp"
|
|
5436
5436
|
]);
|
|
5437
|
+
/** A container `produceEventMedia` can emit. */
|
|
5438
|
+
var EventMediaKindSchema = z.enum(["mp4", "gif"]);
|
|
5439
|
+
/**
|
|
5440
|
+
* One produced artifact, referenced by HANDLE.
|
|
5441
|
+
*
|
|
5442
|
+
* Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
|
|
5443
|
+
* method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
|
|
5444
|
+
* [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
|
|
5445
|
+
* on demand, compressed, by handle). `bytes` is here so a caller can decide
|
|
5446
|
+
* whether it wants the fetch at all.
|
|
5447
|
+
*/
|
|
5448
|
+
var EventMediaArtifactSchema = z.object({
|
|
5449
|
+
kind: EventMediaKindSchema,
|
|
5450
|
+
/** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
|
|
5451
|
+
handle: z.string(),
|
|
5452
|
+
/**
|
|
5453
|
+
* The node holding the bytes — the ROUTING key for `fetchEventMedia`.
|
|
5454
|
+
*
|
|
5455
|
+
* `stream-broker` is a singleton cap and an unpinned call never leaves the
|
|
5456
|
+
* hub, so a handle produced on an agent's broker would be redeemed against
|
|
5457
|
+
* the hub's store and come back `null`. Same contract, same field name and
|
|
5458
|
+
* the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
|
|
5459
|
+
* lives and the consumer pins to it.
|
|
5460
|
+
*/
|
|
5461
|
+
nodeId: z.string(),
|
|
5462
|
+
mime: z.string(),
|
|
5463
|
+
bytes: z.number().int(),
|
|
5464
|
+
width: z.number().int(),
|
|
5465
|
+
height: z.number().int()
|
|
5466
|
+
});
|
|
5467
|
+
/**
|
|
5468
|
+
* What a production actually covered — the answer to the only question an
|
|
5469
|
+
* operator asks about a notification clip.
|
|
5470
|
+
*
|
|
5471
|
+
* `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
|
|
5472
|
+
* so a caller can state "this clip starts 4.1 s before the event" instead of
|
|
5473
|
+
* inferring it from a duration. A production whose `fromTs` is later than the
|
|
5474
|
+
* event is a production with no pre-roll, and that is exactly the defect this
|
|
5475
|
+
* method exists to make visible rather than plausible.
|
|
5476
|
+
*/
|
|
5477
|
+
var EventMediaCoverageSchema = z.object({
|
|
5478
|
+
fromTs: z.number(),
|
|
5479
|
+
toTs: z.number(),
|
|
5480
|
+
/** Encoded packets in the muxed window. */
|
|
5481
|
+
packets: z.number().int()
|
|
5482
|
+
});
|
|
5483
|
+
/**
|
|
5484
|
+
* The result of ONE cut, in every container the caller asked for.
|
|
5485
|
+
*
|
|
5486
|
+
* Every artifact in `media` came out of the SAME window of the SAME rendition —
|
|
5487
|
+
* that is the whole reason this is one method rather than one call per format.
|
|
5488
|
+
* A consumer attaching a gif and a video can no longer show two different
|
|
5489
|
+
* moments, because it never chose two sources.
|
|
5490
|
+
*/
|
|
5491
|
+
var EventMediaProductionSchema = z.object({
|
|
5492
|
+
media: z.array(EventMediaArtifactSchema).readonly(),
|
|
5493
|
+
coverage: EventMediaCoverageSchema,
|
|
5494
|
+
/** The rendition actually cut from — what the default or the fallback chose. */
|
|
5495
|
+
profile: CamProfileSchema,
|
|
5496
|
+
/** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
|
|
5497
|
+
* source, a downscale, or a playback rate other than 1). */
|
|
5498
|
+
video: z.enum(["copy", "encode"])
|
|
5499
|
+
});
|
|
5437
5500
|
var RtspRestreamEntrySchema = z.object({
|
|
5438
5501
|
brokerId: z.string(),
|
|
5439
5502
|
url: z.string(),
|
|
@@ -5871,6 +5934,88 @@ var streamBrokerCapability = {
|
|
|
5871
5934
|
kind: "mutation",
|
|
5872
5935
|
auth: "admin"
|
|
5873
5936
|
}),
|
|
5937
|
+
/**
|
|
5938
|
+
* THE production point for event media — one window, every container.
|
|
5939
|
+
*
|
|
5940
|
+
* `renderPreBufferClip` renders ONE format per call, so a consumer wanting a
|
|
5941
|
+
* gif and a video made two calls and got two windows. On 2026-08-08 the
|
|
5942
|
+
* operator received exactly that: a gif around a doorbell press and an mp4
|
|
5943
|
+
* of the six seconds after it. This method cuts once and derives the rest,
|
|
5944
|
+
* so "the attachments agree" is a property of the service instead of a
|
|
5945
|
+
* discipline every consumer has to keep.
|
|
5946
|
+
*
|
|
5947
|
+
* It is also the only place that owns the packets. The broker already
|
|
5948
|
+
* retains an AnnexB ring per camera (`setClipRetention`, D32) — the
|
|
5949
|
+
* prebuffer — so a production reaches BACKWARDS from the event with no
|
|
5950
|
+
* process to spawn and nothing kept warm. It waits out `postSeconds`, muxes
|
|
5951
|
+
* the window once (COPYING the camera's own H.264 whenever nothing is being
|
|
5952
|
+
* changed), derives any gif from that same mux, and returns HANDLES.
|
|
5953
|
+
*
|
|
5954
|
+
* Intended for every future consumer, not just notifications: a clips API, a
|
|
5955
|
+
* retrain export and an event export all want the same window of the same
|
|
5956
|
+
* camera and must not each grow their own renderer.
|
|
5957
|
+
*/
|
|
5958
|
+
produceEventMedia: method(z.object({
|
|
5959
|
+
deviceId: z.number(),
|
|
5960
|
+
/** Absent = the largest H.264 rendition at or below 1080p, which is
|
|
5961
|
+
* also the one that can be copied. Falls back to whatever the ring
|
|
5962
|
+
* actually retained, and the answer says which. */
|
|
5963
|
+
profile: CamProfileSchema.optional(),
|
|
5964
|
+
aroundMs: z.number(),
|
|
5965
|
+
preSeconds: z.number().min(0).max(20).default(4),
|
|
5966
|
+
postSeconds: z.number().min(0).max(20).default(6),
|
|
5967
|
+
kinds: z.array(EventMediaKindSchema).min(1).default(["mp4"]),
|
|
5968
|
+
/** GIF geometry. The video keeps the source's own. */
|
|
5969
|
+
gifMaxWidth: z.number().int().min(120).max(1280).default(640),
|
|
5970
|
+
/**
|
|
5971
|
+
* The gif's own PLAYBACK rate in frames per second — what the finished
|
|
5972
|
+
* gif runs at, not how many source frames feed it. The decimation that
|
|
5973
|
+
* feeds it samples `gifFps / gifSpeed` source frames per second, so at
|
|
5974
|
+
* the defaults a 12 fps gif is built out of 3 source frames a second.
|
|
5975
|
+
*/
|
|
5976
|
+
gifFps: z.number().int().min(1).max(15).default(12),
|
|
5977
|
+
/**
|
|
5978
|
+
* How fast the GIF plays against real time, independent of `speed`.
|
|
5979
|
+
*
|
|
5980
|
+
* 4× by default, by operator request: a notification gif is glanced at
|
|
5981
|
+
* on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
|
|
5982
|
+
* a separate knob from `speed` even though both now default to 4 —
|
|
5983
|
+
* a caller wanting a real-time video and a fast gif must not have to
|
|
5984
|
+
* choose.
|
|
5985
|
+
*/
|
|
5986
|
+
gifSpeed: z.number().min(1).max(8).default(4),
|
|
5987
|
+
/**
|
|
5988
|
+
* Playback rate of the VIDEO. Also 4× by default, by operator decision.
|
|
5989
|
+
*
|
|
5990
|
+
* `1` is real time and is the ONLY value that allows the copy branch —
|
|
5991
|
+
* anything else forces `libx264` over the window. That was priced
|
|
5992
|
+
* before it was chosen: a per-event burst measured at 0.23 s and 254 KB
|
|
5993
|
+
* on a real 615 720p cut, against 922 KB for the copy it replaces. A
|
|
5994
|
+
* re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
|
|
5995
|
+
* once the decode is forced the width stops being free.
|
|
5996
|
+
*/
|
|
5997
|
+
speed: z.number().min(1).max(8).default(4)
|
|
5998
|
+
}), EventMediaProductionSchema, {
|
|
5999
|
+
kind: "mutation",
|
|
6000
|
+
auth: "admin"
|
|
6001
|
+
}),
|
|
6002
|
+
/**
|
|
6003
|
+
* Redeem a {@link EventMediaArtifactSchema} handle for its bytes.
|
|
6004
|
+
*
|
|
6005
|
+
* Separate from the production on purpose (D18): the producing runner keeps
|
|
6006
|
+
* the artifact for a few minutes, and a consumer that only wanted the
|
|
6007
|
+
* coverage never moves a megabyte. `null` once it has expired — a handle is
|
|
6008
|
+
* short-lived by design and a caller that waited too long must see that
|
|
6009
|
+
* rather than a zero-length file.
|
|
6010
|
+
*/
|
|
6011
|
+
fetchEventMedia: method(z.object({ handle: z.string() }), z.object({
|
|
6012
|
+
base64: z.string(),
|
|
6013
|
+
mime: z.string(),
|
|
6014
|
+
bytes: z.number().int()
|
|
6015
|
+
}).nullable(), {
|
|
6016
|
+
kind: "mutation",
|
|
6017
|
+
auth: "admin"
|
|
6018
|
+
}),
|
|
5874
6019
|
listAllCameraStreams: method(z.void(), z.array(CameraStreamSchema).readonly()),
|
|
5875
6020
|
listAllProfileSlots: method(z.void(), z.array(ProfileSlotSchema).readonly()),
|
|
5876
6021
|
getBrokerStats: method(z.object({ brokerId: z.string() }), BrokerStatsSchema),
|
|
@@ -6442,6 +6587,28 @@ function kebabToCamel(s) {
|
|
|
6442
6587
|
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
6443
6588
|
* must stay so.
|
|
6444
6589
|
*/
|
|
6590
|
+
/**
|
|
6591
|
+
* Prefix for a block's generated addon id AND its runner id — one addon, one
|
|
6592
|
+
* process, so the two are the same string (D2).
|
|
6593
|
+
*
|
|
6594
|
+
* It lives on the CONTRACT rather than beside the code that materialises the
|
|
6595
|
+
* package, because it is not an implementation detail: the operator surface
|
|
6596
|
+
* reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
|
|
6597
|
+
* with no cap and no store of its own. Two copies of this string is one copy
|
|
6598
|
+
* too many — a drift would show as a logs pane that is simply always empty.
|
|
6599
|
+
*/
|
|
6600
|
+
var CORE_BLOCK_ADDON_PREFIX = "core-block-";
|
|
6601
|
+
/** The addon/runner id a block's process runs under. */
|
|
6602
|
+
function coreBlockAddonId(blockId) {
|
|
6603
|
+
return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
|
|
6604
|
+
}
|
|
6605
|
+
/** The block id behind a generated addon/runner id, or null when the id belongs
|
|
6606
|
+
* to something else. The inverse of {@link coreBlockAddonId}. */
|
|
6607
|
+
function coreBlockIdFromAddonId(addonId) {
|
|
6608
|
+
if (!addonId.startsWith("core-block-")) return null;
|
|
6609
|
+
const rest = addonId.slice(11);
|
|
6610
|
+
return rest.length > 0 ? rest : null;
|
|
6611
|
+
}
|
|
6445
6612
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
6446
6613
|
* agent is the reason placement is not fixed to the hub. */
|
|
6447
6614
|
var CoreBlockPlacementSchema = z.union([z.literal("hub"), z.string().min(1)]);
|
|
@@ -6530,6 +6697,23 @@ var coreBlocksCapability = {
|
|
|
6530
6697
|
auth: "admin"
|
|
6531
6698
|
}),
|
|
6532
6699
|
/**
|
|
6700
|
+
* Stop this block's runner and let the supervisor bring it back.
|
|
6701
|
+
*
|
|
6702
|
+
* **It stores nothing.** There is no "restart requested" flag and no second
|
|
6703
|
+
* lifecycle field: the supervisor already stops and respawns a runner when
|
|
6704
|
+
* the code hash changes, and this exposes that same path deliberately
|
|
6705
|
+
* (D62 — a switch writes the authority that already owned the function).
|
|
6706
|
+
* The next reconcile pass remains the authority, so a restart that lost a
|
|
6707
|
+
* race is corrected within one pass rather than leaving a stale flag behind.
|
|
6708
|
+
*
|
|
6709
|
+
* Refused for a DISABLED block: nothing would come back, and a button that
|
|
6710
|
+
* silently does nothing is worse than one that says why.
|
|
6711
|
+
*/
|
|
6712
|
+
restart: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema }), {
|
|
6713
|
+
kind: "mutation",
|
|
6714
|
+
auth: "admin"
|
|
6715
|
+
}),
|
|
6716
|
+
/**
|
|
6533
6717
|
* Type-check without saving — what the editor calls as the author types, so
|
|
6534
6718
|
* the compiler's verdict is the same one the server will reach.
|
|
6535
6719
|
*/
|
|
@@ -7570,895 +7754,159 @@ var deviceExportCapability = {
|
|
|
7570
7754
|
unexposeDevice: method(UnexposeInputSchema, z.void(), { kind: "mutation" })
|
|
7571
7755
|
}
|
|
7572
7756
|
};
|
|
7757
|
+
var ProviderStatusSchema = z.object({
|
|
7758
|
+
connected: z.boolean(),
|
|
7759
|
+
deviceCount: z.number(),
|
|
7760
|
+
error: z.string().optional()
|
|
7761
|
+
});
|
|
7762
|
+
z.object({
|
|
7763
|
+
externalId: z.string(),
|
|
7764
|
+
name: z.string(),
|
|
7765
|
+
type: z.string(),
|
|
7766
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
7767
|
+
});
|
|
7573
7768
|
/**
|
|
7574
|
-
*
|
|
7575
|
-
*
|
|
7576
|
-
*
|
|
7577
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
7578
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
7579
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
7580
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
7769
|
+
* Candidate handed back from discovery and accepted by
|
|
7770
|
+
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
7771
|
+
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
7581
7772
|
*/
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
/**
|
|
7588
|
-
*
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7773
|
+
var DiscoveryCandidateSchema = z.object({
|
|
7774
|
+
stableId: z.string(),
|
|
7775
|
+
type: z.enum(DeviceType),
|
|
7776
|
+
suggestedName: z.string(),
|
|
7777
|
+
prefilledConfig: z.record(z.string(), z.unknown()),
|
|
7778
|
+
/**
|
|
7779
|
+
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
7780
|
+
* Discovery pre-populates this for systems that know the upstream
|
|
7781
|
+
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
7782
|
+
* flows live through the cap STATUS SLICE after adoption.
|
|
7783
|
+
*/
|
|
7784
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
7785
|
+
});
|
|
7595
7786
|
/**
|
|
7596
|
-
*
|
|
7597
|
-
*
|
|
7598
|
-
*
|
|
7787
|
+
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
7788
|
+
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
7789
|
+
* tRPC layer can pass it through without reshaping.
|
|
7599
7790
|
*/
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
/**
|
|
7611
|
-
*
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7791
|
+
var DeviceSummarySchema = z.object({
|
|
7792
|
+
id: z.number(),
|
|
7793
|
+
stableId: z.string(),
|
|
7794
|
+
addonId: z.string(),
|
|
7795
|
+
type: z.string(),
|
|
7796
|
+
name: z.string(),
|
|
7797
|
+
parentDeviceId: z.number().nullable(),
|
|
7798
|
+
online: z.boolean(),
|
|
7799
|
+
features: z.array(z.string()),
|
|
7800
|
+
config: z.record(z.string(), z.unknown()),
|
|
7801
|
+
/** Optional upstream-system identity (dispatch key + system tag).
|
|
7802
|
+
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
7803
|
+
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
7804
|
+
* synthetic backfill is in effect. */
|
|
7805
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
7806
|
+
});
|
|
7807
|
+
/**
|
|
7808
|
+
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
7809
|
+
* creation). Matches the UI-side `FieldProbeResult` in
|
|
7810
|
+
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
7811
|
+
* returned `labels` as chips next to the input.
|
|
7812
|
+
*/
|
|
7813
|
+
var FieldProbeResultSchema = z.object({
|
|
7814
|
+
status: z.enum(["ok", "error"]),
|
|
7815
|
+
labels: z.array(z.string()).optional(),
|
|
7816
|
+
error: z.string().optional()
|
|
7817
|
+
});
|
|
7818
|
+
/**
|
|
7819
|
+
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
7820
|
+
* it as `unknown` at the capability layer — the router just passes it
|
|
7821
|
+
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
7822
|
+
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
7823
|
+
* but we deliberately avoid a Zod mirror because the union is large and
|
|
7824
|
+
* not meant for runtime validation at this seam.
|
|
7825
|
+
*/
|
|
7826
|
+
var CreationSchemaOutputSchema = z.unknown();
|
|
7827
|
+
var deviceProviderCapability = {
|
|
7828
|
+
name: "device-provider",
|
|
7829
|
+
scope: "system",
|
|
7830
|
+
mode: "collection",
|
|
7831
|
+
methods: {
|
|
7832
|
+
start: method(z.void(), z.void(), { kind: "mutation" }),
|
|
7833
|
+
stop: method(z.void(), z.void(), { kind: "mutation" }),
|
|
7834
|
+
getStatus: method(z.void(), ProviderStatusSchema),
|
|
7835
|
+
getDevices: method(z.void(), z.array(z.object({
|
|
7836
|
+
id: z.string(),
|
|
7837
|
+
name: z.string(),
|
|
7838
|
+
type: z.string()
|
|
7839
|
+
}))),
|
|
7840
|
+
supportsDiscovery: method(z.object({}), z.boolean()),
|
|
7841
|
+
/**
|
|
7842
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
7843
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
7844
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
7845
|
+
* (provider uses its local-network default).
|
|
7846
|
+
*/
|
|
7847
|
+
discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
|
|
7848
|
+
kind: "mutation",
|
|
7849
|
+
auth: "admin"
|
|
7850
|
+
}),
|
|
7851
|
+
/**
|
|
7852
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
7853
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
7854
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
7855
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
7856
|
+
*/
|
|
7857
|
+
getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
|
|
7858
|
+
/**
|
|
7859
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
7860
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
7861
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
7862
|
+
* pick the right type instead of assuming Camera.
|
|
7863
|
+
*/
|
|
7864
|
+
getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
|
|
7865
|
+
adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
7866
|
+
kind: "mutation",
|
|
7867
|
+
auth: "admin"
|
|
7868
|
+
}),
|
|
7869
|
+
supportsManualCreation: method(z.object({}), z.boolean()),
|
|
7870
|
+
/**
|
|
7871
|
+
* Fetch the creation form schema for a given DeviceType. Returns
|
|
7872
|
+
* `null` when the provider does not support manually creating
|
|
7873
|
+
* devices of that type. The output is a `ConfigUISchema` — the
|
|
7874
|
+
* router type-asserts it at the boundary.
|
|
7875
|
+
*/
|
|
7876
|
+
getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
7877
|
+
createDevice: method(z.object({
|
|
7878
|
+
type: z.enum(DeviceType),
|
|
7879
|
+
config: z.record(z.string(), z.unknown())
|
|
7880
|
+
}), DeviceSummarySchema, {
|
|
7881
|
+
kind: "mutation",
|
|
7882
|
+
auth: "admin"
|
|
7883
|
+
}),
|
|
7884
|
+
/**
|
|
7885
|
+
* Test a single field in the creation form before the device has
|
|
7886
|
+
* been persisted. Typical use: probing an RTSP URL entered by the
|
|
7887
|
+
* user. Providers that don't support field probing return
|
|
7888
|
+
* `{ success: true, message: 'Field test not supported' }`.
|
|
7889
|
+
*
|
|
7890
|
+
* `formValues` is the live snapshot of every field in the form at
|
|
7891
|
+
* the moment the user clicked Test — useful for probes that depend
|
|
7892
|
+
* on multiple fields together (e.g. Reolink autodetect needs host
|
|
7893
|
+
* + credentials + UID + transport mode in a single call). Optional
|
|
7894
|
+
* for backwards compatibility; providers free to ignore it.
|
|
7895
|
+
*/
|
|
7896
|
+
testCreationField: method(z.object({
|
|
7897
|
+
type: z.enum(DeviceType),
|
|
7898
|
+
key: z.string(),
|
|
7899
|
+
value: z.unknown(),
|
|
7900
|
+
formValues: z.record(z.string(), z.unknown()).optional()
|
|
7901
|
+
}), FieldProbeResultSchema, {
|
|
7902
|
+
kind: "mutation",
|
|
7903
|
+
auth: "admin"
|
|
7904
|
+
})
|
|
7616
7905
|
}
|
|
7617
7906
|
};
|
|
7618
7907
|
/**
|
|
7619
|
-
*
|
|
7620
|
-
*
|
|
7621
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
7622
|
-
* own-property check against it.
|
|
7623
|
-
*
|
|
7624
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
7625
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
7626
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
7627
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
7628
|
-
* callable — they are simply "unknown function" at parse time.
|
|
7629
|
-
*
|
|
7630
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
7631
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
7632
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
7633
|
-
* closed rather than emitting a garbage value.
|
|
7634
|
-
*/
|
|
7635
|
-
function asFiniteNumber(value, name, index) {
|
|
7636
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
7637
|
-
return value;
|
|
7638
|
-
}
|
|
7639
|
-
function asString$1(value, name, index) {
|
|
7640
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
7641
|
-
return value;
|
|
7642
|
-
}
|
|
7643
|
-
function finiteResult(value, name) {
|
|
7644
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
7645
|
-
return value;
|
|
7646
|
-
}
|
|
7647
|
-
function allFiniteNumbers(args, name) {
|
|
7648
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
7649
|
-
}
|
|
7650
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
7651
|
-
var table = {
|
|
7652
|
-
min: {
|
|
7653
|
-
minArgs: 1,
|
|
7654
|
-
maxArgs: INF,
|
|
7655
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
7656
|
-
},
|
|
7657
|
-
max: {
|
|
7658
|
-
minArgs: 1,
|
|
7659
|
-
maxArgs: INF,
|
|
7660
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
7661
|
-
},
|
|
7662
|
-
abs: {
|
|
7663
|
-
minArgs: 1,
|
|
7664
|
-
maxArgs: 1,
|
|
7665
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
7666
|
-
},
|
|
7667
|
-
floor: {
|
|
7668
|
-
minArgs: 1,
|
|
7669
|
-
maxArgs: 1,
|
|
7670
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
7671
|
-
},
|
|
7672
|
-
ceil: {
|
|
7673
|
-
minArgs: 1,
|
|
7674
|
-
maxArgs: 1,
|
|
7675
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
7676
|
-
},
|
|
7677
|
-
sqrt: {
|
|
7678
|
-
minArgs: 1,
|
|
7679
|
-
maxArgs: 1,
|
|
7680
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
7681
|
-
},
|
|
7682
|
-
round: {
|
|
7683
|
-
minArgs: 1,
|
|
7684
|
-
maxArgs: 2,
|
|
7685
|
-
apply: (args) => {
|
|
7686
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
7687
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
7688
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
7689
|
-
const factor = 10 ** digits;
|
|
7690
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
7691
|
-
}
|
|
7692
|
-
},
|
|
7693
|
-
pow: {
|
|
7694
|
-
minArgs: 2,
|
|
7695
|
-
maxArgs: 2,
|
|
7696
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
7697
|
-
},
|
|
7698
|
-
clamp: {
|
|
7699
|
-
minArgs: 3,
|
|
7700
|
-
maxArgs: 3,
|
|
7701
|
-
apply: (args) => {
|
|
7702
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
7703
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
7704
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
7705
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
7706
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
7707
|
-
}
|
|
7708
|
-
},
|
|
7709
|
-
avg: {
|
|
7710
|
-
minArgs: 1,
|
|
7711
|
-
maxArgs: INF,
|
|
7712
|
-
apply: (args) => {
|
|
7713
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
7714
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
7715
|
-
}
|
|
7716
|
-
},
|
|
7717
|
-
sum: {
|
|
7718
|
-
minArgs: 1,
|
|
7719
|
-
maxArgs: INF,
|
|
7720
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
7721
|
-
},
|
|
7722
|
-
coalesce: {
|
|
7723
|
-
minArgs: 1,
|
|
7724
|
-
maxArgs: INF,
|
|
7725
|
-
apply: (args) => {
|
|
7726
|
-
for (const a of args) if (a !== null) return a;
|
|
7727
|
-
return null;
|
|
7728
|
-
}
|
|
7729
|
-
},
|
|
7730
|
-
age: {
|
|
7731
|
-
minArgs: 2,
|
|
7732
|
-
maxArgs: 2,
|
|
7733
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
7734
|
-
},
|
|
7735
|
-
convert: {
|
|
7736
|
-
minArgs: 3,
|
|
7737
|
-
maxArgs: 3,
|
|
7738
|
-
apply: (args, hooks) => {
|
|
7739
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
7740
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
7741
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
7742
|
-
if (hooks.convert) {
|
|
7743
|
-
const out = hooks.convert(x, from, to);
|
|
7744
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
7745
|
-
return finiteResult(out, "convert");
|
|
7746
|
-
}
|
|
7747
|
-
if (from === to) return x;
|
|
7748
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
7749
|
-
}
|
|
7750
|
-
}
|
|
7751
|
-
};
|
|
7752
|
-
/** Frozen, null-prototype builtin table. */
|
|
7753
|
-
var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
|
|
7754
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
7755
|
-
* callees at parse time (immediate author feedback). */
|
|
7756
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
7757
|
-
/**
|
|
7758
|
-
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
7759
|
-
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
7760
|
-
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
7761
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
7762
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
7763
|
-
* is a parse error with a source position, so member access / assignment /
|
|
7764
|
-
* template literals are lexically impossible.
|
|
7765
|
-
*/
|
|
7766
|
-
var KEYWORDS = new Set([
|
|
7767
|
-
"true",
|
|
7768
|
-
"false",
|
|
7769
|
-
"null"
|
|
7770
|
-
]);
|
|
7771
|
-
function isDigit(ch) {
|
|
7772
|
-
return ch >= "0" && ch <= "9";
|
|
7773
|
-
}
|
|
7774
|
-
function isIdentStart(ch) {
|
|
7775
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
7776
|
-
}
|
|
7777
|
-
function isIdentPart(ch) {
|
|
7778
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
7779
|
-
}
|
|
7780
|
-
function isWhitespace(ch) {
|
|
7781
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
7782
|
-
}
|
|
7783
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
7784
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
7785
|
-
* string. */
|
|
7786
|
-
function tokenize(source) {
|
|
7787
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
7788
|
-
const tokens = [];
|
|
7789
|
-
let i = 0;
|
|
7790
|
-
const n = source.length;
|
|
7791
|
-
while (i < n) {
|
|
7792
|
-
const ch = source[i];
|
|
7793
|
-
if (isWhitespace(ch)) {
|
|
7794
|
-
i += 1;
|
|
7795
|
-
continue;
|
|
7796
|
-
}
|
|
7797
|
-
if (isDigit(ch)) {
|
|
7798
|
-
const start = i;
|
|
7799
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
7800
|
-
if (i < n && source[i] === ".") {
|
|
7801
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
7802
|
-
i += 1;
|
|
7803
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
7804
|
-
}
|
|
7805
|
-
const text = source.slice(start, i);
|
|
7806
|
-
const value = Number(text);
|
|
7807
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
7808
|
-
tokens.push({
|
|
7809
|
-
type: "number",
|
|
7810
|
-
value,
|
|
7811
|
-
pos: start
|
|
7812
|
-
});
|
|
7813
|
-
continue;
|
|
7814
|
-
}
|
|
7815
|
-
if (ch === "'" || ch === "\"") {
|
|
7816
|
-
const quote = ch;
|
|
7817
|
-
const start = i;
|
|
7818
|
-
i += 1;
|
|
7819
|
-
let out = "";
|
|
7820
|
-
let closed = false;
|
|
7821
|
-
while (i < n) {
|
|
7822
|
-
const c = source[i];
|
|
7823
|
-
if (c === "\\") {
|
|
7824
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
7825
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
7826
|
-
out += next;
|
|
7827
|
-
i += 2;
|
|
7828
|
-
continue;
|
|
7829
|
-
}
|
|
7830
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
7831
|
-
}
|
|
7832
|
-
if (c === quote) {
|
|
7833
|
-
closed = true;
|
|
7834
|
-
i += 1;
|
|
7835
|
-
break;
|
|
7836
|
-
}
|
|
7837
|
-
out += c;
|
|
7838
|
-
i += 1;
|
|
7839
|
-
}
|
|
7840
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
7841
|
-
tokens.push({
|
|
7842
|
-
type: "string",
|
|
7843
|
-
value: out,
|
|
7844
|
-
pos: start
|
|
7845
|
-
});
|
|
7846
|
-
continue;
|
|
7847
|
-
}
|
|
7848
|
-
if (isIdentStart(ch)) {
|
|
7849
|
-
const start = i;
|
|
7850
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
7851
|
-
const text = source.slice(start, i);
|
|
7852
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
7853
|
-
type: "keyword",
|
|
7854
|
-
keyword: keywordOf(text),
|
|
7855
|
-
pos: start
|
|
7856
|
-
});
|
|
7857
|
-
else tokens.push({
|
|
7858
|
-
type: "identifier",
|
|
7859
|
-
name: text,
|
|
7860
|
-
pos: start
|
|
7861
|
-
});
|
|
7862
|
-
continue;
|
|
7863
|
-
}
|
|
7864
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
7865
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
7866
|
-
tokens.push({
|
|
7867
|
-
type: "punct",
|
|
7868
|
-
punct: two,
|
|
7869
|
-
pos: i
|
|
7870
|
-
});
|
|
7871
|
-
i += 2;
|
|
7872
|
-
continue;
|
|
7873
|
-
}
|
|
7874
|
-
if (isSinglePunct(ch)) {
|
|
7875
|
-
tokens.push({
|
|
7876
|
-
type: "punct",
|
|
7877
|
-
punct: ch,
|
|
7878
|
-
pos: i
|
|
7879
|
-
});
|
|
7880
|
-
i += 1;
|
|
7881
|
-
continue;
|
|
7882
|
-
}
|
|
7883
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
7884
|
-
}
|
|
7885
|
-
tokens.push({
|
|
7886
|
-
type: "eof",
|
|
7887
|
-
pos: n
|
|
7888
|
-
});
|
|
7889
|
-
return tokens;
|
|
7890
|
-
}
|
|
7891
|
-
function keywordOf(text) {
|
|
7892
|
-
if (text === "true") return "true";
|
|
7893
|
-
if (text === "false") return "false";
|
|
7894
|
-
return "null";
|
|
7895
|
-
}
|
|
7896
|
-
function isSinglePunct(ch) {
|
|
7897
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
7898
|
-
}
|
|
7899
|
-
/**
|
|
7900
|
-
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
7901
|
-
*
|
|
7902
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
7903
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
7904
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
7905
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
7906
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
7907
|
-
* that references a since-removed builtin degrades at read.
|
|
7908
|
-
*
|
|
7909
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
7910
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
7911
|
-
*/
|
|
7912
|
-
/** Binary/logical operator precedence (higher binds tighter). */
|
|
7913
|
-
var BINARY_PRECEDENCE = {
|
|
7914
|
-
"||": 1,
|
|
7915
|
-
"&&": 2,
|
|
7916
|
-
"==": 3,
|
|
7917
|
-
"!=": 3,
|
|
7918
|
-
"<": 4,
|
|
7919
|
-
"<=": 4,
|
|
7920
|
-
">": 4,
|
|
7921
|
-
">=": 4,
|
|
7922
|
-
"+": 5,
|
|
7923
|
-
"-": 5,
|
|
7924
|
-
"*": 6,
|
|
7925
|
-
"/": 6,
|
|
7926
|
-
"%": 6
|
|
7927
|
-
};
|
|
7928
|
-
function isLogicalOp(op) {
|
|
7929
|
-
return op === "&&" || op === "||";
|
|
7930
|
-
}
|
|
7931
|
-
function isBinaryOp(op) {
|
|
7932
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
7933
|
-
}
|
|
7934
|
-
var Parser = class {
|
|
7935
|
-
tokens;
|
|
7936
|
-
pos = 0;
|
|
7937
|
-
nodeCount = 0;
|
|
7938
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
7939
|
-
callees = /* @__PURE__ */ new Set();
|
|
7940
|
-
constructor(tokens) {
|
|
7941
|
-
this.tokens = tokens;
|
|
7942
|
-
}
|
|
7943
|
-
parse() {
|
|
7944
|
-
const ast = this.parseTernary();
|
|
7945
|
-
const tok = this.peek();
|
|
7946
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
7947
|
-
return {
|
|
7948
|
-
ast,
|
|
7949
|
-
identifiers: this.identifiers,
|
|
7950
|
-
callees: this.callees,
|
|
7951
|
-
nodeCount: this.nodeCount
|
|
7952
|
-
};
|
|
7953
|
-
}
|
|
7954
|
-
peek() {
|
|
7955
|
-
return this.tokens[this.pos];
|
|
7956
|
-
}
|
|
7957
|
-
next() {
|
|
7958
|
-
return this.tokens[this.pos++];
|
|
7959
|
-
}
|
|
7960
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
7961
|
-
expectPunct(punct) {
|
|
7962
|
-
const tok = this.peek();
|
|
7963
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
7964
|
-
this.pos += 1;
|
|
7965
|
-
}
|
|
7966
|
-
matchPunct(punct) {
|
|
7967
|
-
const tok = this.peek();
|
|
7968
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
7969
|
-
this.pos += 1;
|
|
7970
|
-
return true;
|
|
7971
|
-
}
|
|
7972
|
-
return false;
|
|
7973
|
-
}
|
|
7974
|
-
countNode() {
|
|
7975
|
-
this.nodeCount += 1;
|
|
7976
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
7977
|
-
}
|
|
7978
|
-
parseTernary() {
|
|
7979
|
-
const test = this.parseBinary(1);
|
|
7980
|
-
if (this.matchPunct("?")) {
|
|
7981
|
-
const consequent = this.parseTernary();
|
|
7982
|
-
this.expectPunct(":");
|
|
7983
|
-
const alternate = this.parseTernary();
|
|
7984
|
-
this.countNode();
|
|
7985
|
-
return {
|
|
7986
|
-
kind: "conditional",
|
|
7987
|
-
test,
|
|
7988
|
-
consequent,
|
|
7989
|
-
alternate
|
|
7990
|
-
};
|
|
7991
|
-
}
|
|
7992
|
-
return test;
|
|
7993
|
-
}
|
|
7994
|
-
parseBinary(minPrec) {
|
|
7995
|
-
let left = this.parseUnary();
|
|
7996
|
-
for (;;) {
|
|
7997
|
-
const tok = this.peek();
|
|
7998
|
-
if (tok.type !== "punct") break;
|
|
7999
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
8000
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
8001
|
-
const op = tok.punct;
|
|
8002
|
-
this.pos += 1;
|
|
8003
|
-
const right = this.parseBinary(prec + 1);
|
|
8004
|
-
this.countNode();
|
|
8005
|
-
if (isLogicalOp(op)) left = {
|
|
8006
|
-
kind: "logical",
|
|
8007
|
-
op,
|
|
8008
|
-
left,
|
|
8009
|
-
right
|
|
8010
|
-
};
|
|
8011
|
-
else if (isBinaryOp(op)) left = {
|
|
8012
|
-
kind: "binary",
|
|
8013
|
-
op,
|
|
8014
|
-
left,
|
|
8015
|
-
right
|
|
8016
|
-
};
|
|
8017
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
8018
|
-
}
|
|
8019
|
-
return left;
|
|
8020
|
-
}
|
|
8021
|
-
parseUnary() {
|
|
8022
|
-
const tok = this.peek();
|
|
8023
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
8024
|
-
const op = tok.punct;
|
|
8025
|
-
this.pos += 1;
|
|
8026
|
-
const operand = this.parseUnary();
|
|
8027
|
-
this.countNode();
|
|
8028
|
-
return {
|
|
8029
|
-
kind: "unary",
|
|
8030
|
-
op,
|
|
8031
|
-
operand
|
|
8032
|
-
};
|
|
8033
|
-
}
|
|
8034
|
-
return this.parsePrimary();
|
|
8035
|
-
}
|
|
8036
|
-
parsePrimary() {
|
|
8037
|
-
const tok = this.next();
|
|
8038
|
-
switch (tok.type) {
|
|
8039
|
-
case "number":
|
|
8040
|
-
this.countNode();
|
|
8041
|
-
return {
|
|
8042
|
-
kind: "literal",
|
|
8043
|
-
value: tok.value
|
|
8044
|
-
};
|
|
8045
|
-
case "string":
|
|
8046
|
-
this.countNode();
|
|
8047
|
-
return {
|
|
8048
|
-
kind: "literal",
|
|
8049
|
-
value: tok.value
|
|
8050
|
-
};
|
|
8051
|
-
case "keyword":
|
|
8052
|
-
this.countNode();
|
|
8053
|
-
return {
|
|
8054
|
-
kind: "literal",
|
|
8055
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
8056
|
-
};
|
|
8057
|
-
case "identifier": {
|
|
8058
|
-
const nextTok = this.peek();
|
|
8059
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
8060
|
-
this.identifiers.add(tok.name);
|
|
8061
|
-
this.countNode();
|
|
8062
|
-
return {
|
|
8063
|
-
kind: "identifier",
|
|
8064
|
-
name: tok.name
|
|
8065
|
-
};
|
|
8066
|
-
}
|
|
8067
|
-
case "punct":
|
|
8068
|
-
if (tok.punct === "(") {
|
|
8069
|
-
const inner = this.parseTernary();
|
|
8070
|
-
this.expectPunct(")");
|
|
8071
|
-
return inner;
|
|
8072
|
-
}
|
|
8073
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
8074
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
8075
|
-
}
|
|
8076
|
-
}
|
|
8077
|
-
parseCall(callee, pos) {
|
|
8078
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
8079
|
-
this.expectPunct("(");
|
|
8080
|
-
const args = [];
|
|
8081
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
8082
|
-
args.push(this.parseTernary());
|
|
8083
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
8084
|
-
if (this.matchPunct(",")) continue;
|
|
8085
|
-
this.expectPunct(")");
|
|
8086
|
-
break;
|
|
8087
|
-
}
|
|
8088
|
-
this.callees.add(callee);
|
|
8089
|
-
this.countNode();
|
|
8090
|
-
return {
|
|
8091
|
-
kind: "call",
|
|
8092
|
-
callee,
|
|
8093
|
-
args
|
|
8094
|
-
};
|
|
8095
|
-
}
|
|
8096
|
-
};
|
|
8097
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
8098
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
8099
|
-
function parseExpression(source) {
|
|
8100
|
-
return new Parser(tokenize(source)).parse();
|
|
8101
|
-
}
|
|
8102
|
-
/**
|
|
8103
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
8104
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
8105
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
8106
|
-
* one per read on a hot resolve path.
|
|
8107
|
-
*
|
|
8108
|
-
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
8109
|
-
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
8110
|
-
* callers is safe and maximises hit rate.
|
|
8111
|
-
*/
|
|
8112
|
-
var cache = /* @__PURE__ */ new Map();
|
|
8113
|
-
function getCached(source) {
|
|
8114
|
-
const hit = cache.get(source);
|
|
8115
|
-
if (hit !== void 0) {
|
|
8116
|
-
cache.delete(source);
|
|
8117
|
-
cache.set(source, hit);
|
|
8118
|
-
return hit;
|
|
8119
|
-
}
|
|
8120
|
-
let result;
|
|
8121
|
-
try {
|
|
8122
|
-
result = {
|
|
8123
|
-
ok: true,
|
|
8124
|
-
parsed: parseExpression(source)
|
|
8125
|
-
};
|
|
8126
|
-
} catch (err) {
|
|
8127
|
-
result = {
|
|
8128
|
-
ok: false,
|
|
8129
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
8130
|
-
};
|
|
8131
|
-
}
|
|
8132
|
-
cache.set(source, result);
|
|
8133
|
-
if (cache.size > 256) {
|
|
8134
|
-
const oldest = cache.keys().next().value;
|
|
8135
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
8136
|
-
}
|
|
8137
|
-
return result;
|
|
8138
|
-
}
|
|
8139
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
8140
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
8141
|
-
function compileExpressionSafe(source) {
|
|
8142
|
-
return getCached(source);
|
|
8143
|
-
}
|
|
8144
|
-
/**
|
|
8145
|
-
* Tree-walking evaluator for the safe expression mini-language.
|
|
8146
|
-
*
|
|
8147
|
-
* SECURITY (spec §4 rule 2/5):
|
|
8148
|
-
* - The scope is an `Object.create(null)` copy of ONLY the caller's own
|
|
8149
|
-
* enumerable binding entries, so `name in scope` is a pure own-key check and
|
|
8150
|
-
* `constructor` / `__proto__` / `toString` are plain unknown identifiers.
|
|
8151
|
-
* - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
|
|
8152
|
-
* directly — the only external calls are into the frozen builtin table.
|
|
8153
|
-
* - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
|
|
8154
|
-
* construction; the step counter is defense-in-depth for a crafted max-size
|
|
8155
|
-
* AST. Nothing blocks: there are no timers, awaits or unbounded loops.
|
|
8156
|
-
*/
|
|
8157
|
-
var EMPTY_HOOKS = Object.freeze({});
|
|
8158
|
-
/** Build a null-prototype scope from own-enumerable binding entries. Inherited
|
|
8159
|
-
* keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
|
|
8160
|
-
* so nothing smuggles in via the prototype chain. */
|
|
8161
|
-
function createExpressionScope(bindings) {
|
|
8162
|
-
const scope = Object.create(null);
|
|
8163
|
-
for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
|
|
8164
|
-
return scope;
|
|
8165
|
-
}
|
|
8166
|
-
function isFiniteNumber(value) {
|
|
8167
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
8168
|
-
}
|
|
8169
|
-
/** JS truthiness of a primitive value. */
|
|
8170
|
-
function truthy(value) {
|
|
8171
|
-
return Boolean(value);
|
|
8172
|
-
}
|
|
8173
|
-
function requireFinite(value, context) {
|
|
8174
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
|
|
8175
|
-
return value;
|
|
8176
|
-
}
|
|
8177
|
-
function step(ctx) {
|
|
8178
|
-
ctx.steps += 1;
|
|
8179
|
-
if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
|
|
8180
|
-
}
|
|
8181
|
-
function evalNode(node, ctx) {
|
|
8182
|
-
step(ctx);
|
|
8183
|
-
switch (node.kind) {
|
|
8184
|
-
case "literal": return node.value;
|
|
8185
|
-
case "identifier":
|
|
8186
|
-
if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
|
|
8187
|
-
return ctx.scope[node.name];
|
|
8188
|
-
case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
|
|
8189
|
-
case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
|
|
8190
|
-
case "logical": {
|
|
8191
|
-
const left = evalNode(node.left, ctx);
|
|
8192
|
-
if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
|
|
8193
|
-
return truthy(left) ? left : evalNode(node.right, ctx);
|
|
8194
|
-
}
|
|
8195
|
-
case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
|
|
8196
|
-
case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
|
|
8197
|
-
}
|
|
8198
|
-
}
|
|
8199
|
-
function evalUnary(op, operand) {
|
|
8200
|
-
if (op === "!") return !truthy(operand);
|
|
8201
|
-
if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
|
|
8202
|
-
return requireFinite(-operand, "unary \"-\"");
|
|
8203
|
-
}
|
|
8204
|
-
function evalBinary(op, left, right) {
|
|
8205
|
-
switch (op) {
|
|
8206
|
-
case "==": return left === right;
|
|
8207
|
-
case "!=": return left !== right;
|
|
8208
|
-
case "+":
|
|
8209
|
-
if (typeof left === "string" && typeof right === "string") return left + right;
|
|
8210
|
-
if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
|
|
8211
|
-
throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
|
|
8212
|
-
case "-":
|
|
8213
|
-
case "*":
|
|
8214
|
-
case "/":
|
|
8215
|
-
case "%":
|
|
8216
|
-
if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
|
|
8217
|
-
return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
|
|
8218
|
-
case "<":
|
|
8219
|
-
case "<=":
|
|
8220
|
-
case ">":
|
|
8221
|
-
case ">=":
|
|
8222
|
-
if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
8223
|
-
if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
8224
|
-
throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
|
|
8225
|
-
}
|
|
8226
|
-
}
|
|
8227
|
-
function evalCall(callee, args, hooks) {
|
|
8228
|
-
if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
|
|
8229
|
-
const builtin = EXPRESSION_BUILTINS[callee];
|
|
8230
|
-
if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
|
|
8231
|
-
return builtin.apply(args, hooks);
|
|
8232
|
-
}
|
|
8233
|
-
/** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
|
|
8234
|
-
* runtime failure (unknown identifier, type mismatch, non-finite result,
|
|
8235
|
-
* step-budget exhaustion). */
|
|
8236
|
-
function evaluateAst(node, scope, opts) {
|
|
8237
|
-
return evalNode(node, {
|
|
8238
|
-
scope,
|
|
8239
|
-
hooks: opts?.hooks ?? EMPTY_HOOKS,
|
|
8240
|
-
maxSteps: opts?.maxSteps ?? 4096,
|
|
8241
|
-
steps: 0
|
|
8242
|
-
});
|
|
8243
|
-
}
|
|
8244
|
-
/**
|
|
8245
|
-
* Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
|
|
8246
|
-
* Non-primitive values (objects, arrays, `undefined`, functions, bigint,
|
|
8247
|
-
* symbol) and non-finite numbers become `undefined` so the caller can apply
|
|
8248
|
-
* its binding-miss policy (→ `null`). `null` itself is a valid value.
|
|
8249
|
-
*/
|
|
8250
|
-
function toExpressionValue(raw) {
|
|
8251
|
-
if (raw === null) return null;
|
|
8252
|
-
if (typeof raw === "string") return raw;
|
|
8253
|
-
if (typeof raw === "boolean") return raw;
|
|
8254
|
-
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
8255
|
-
}
|
|
8256
|
-
/**
|
|
8257
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
8258
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
8259
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
8260
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
8261
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
8262
|
-
*/
|
|
8263
|
-
function validateExpressionSource(src) {
|
|
8264
|
-
const names = Object.keys(src.bindings);
|
|
8265
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
8266
|
-
for (const name of names) {
|
|
8267
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
8268
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
8269
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
8270
|
-
}
|
|
8271
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
8272
|
-
if (!compiled.ok) return compiled.error;
|
|
8273
|
-
const bound = new Set(names);
|
|
8274
|
-
for (const id of compiled.parsed.identifiers) {
|
|
8275
|
-
if (id === "now") continue;
|
|
8276
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
8277
|
-
}
|
|
8278
|
-
return null;
|
|
8279
|
-
}
|
|
8280
|
-
/**
|
|
8281
|
-
* Shared read-path evaluation for BOTH resolver channels. Builds a null-proto
|
|
8282
|
-
* scope from `bindingValues` plus the injected `now` (supplied by the caller
|
|
8283
|
-
* for determinism/testability), compiles via the LRU, and evaluates. Any
|
|
8284
|
-
* failure (parse or eval) returns `{ ok: false }` — the caller treats that as
|
|
8285
|
-
* "skip this link".
|
|
8286
|
-
*/
|
|
8287
|
-
function evaluateLinkExpression(expr, bindingValues, now, opts) {
|
|
8288
|
-
const compiled = compileExpressionSafe(expr);
|
|
8289
|
-
if (!compiled.ok) return {
|
|
8290
|
-
ok: false,
|
|
8291
|
-
error: compiled.error
|
|
8292
|
-
};
|
|
8293
|
-
const scope = createExpressionScope({
|
|
8294
|
-
...bindingValues,
|
|
8295
|
-
["now"]: now
|
|
8296
|
-
});
|
|
8297
|
-
try {
|
|
8298
|
-
return {
|
|
8299
|
-
ok: true,
|
|
8300
|
-
value: evaluateAst(compiled.parsed.ast, scope, opts)
|
|
8301
|
-
};
|
|
8302
|
-
} catch (err) {
|
|
8303
|
-
return {
|
|
8304
|
-
ok: false,
|
|
8305
|
-
error: err instanceof ExpressionEvalError ? err.message : String(err)
|
|
8306
|
-
};
|
|
8307
|
-
}
|
|
8308
|
-
}
|
|
8309
|
-
var ProviderStatusSchema = z.object({
|
|
8310
|
-
connected: z.boolean(),
|
|
8311
|
-
deviceCount: z.number(),
|
|
8312
|
-
error: z.string().optional()
|
|
8313
|
-
});
|
|
8314
|
-
z.object({
|
|
8315
|
-
externalId: z.string(),
|
|
8316
|
-
name: z.string(),
|
|
8317
|
-
type: z.string(),
|
|
8318
|
-
metadata: z.record(z.string(), z.unknown()).optional()
|
|
8319
|
-
});
|
|
8320
|
-
/**
|
|
8321
|
-
* Candidate handed back from discovery and accepted by
|
|
8322
|
-
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
8323
|
-
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
8324
|
-
*/
|
|
8325
|
-
var DiscoveryCandidateSchema = z.object({
|
|
8326
|
-
stableId: z.string(),
|
|
8327
|
-
type: z.enum(DeviceType),
|
|
8328
|
-
suggestedName: z.string(),
|
|
8329
|
-
prefilledConfig: z.record(z.string(), z.unknown()),
|
|
8330
|
-
/**
|
|
8331
|
-
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
8332
|
-
* Discovery pre-populates this for systems that know the upstream
|
|
8333
|
-
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
8334
|
-
* flows live through the cap STATUS SLICE after adoption.
|
|
8335
|
-
*/
|
|
8336
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
8337
|
-
});
|
|
8338
|
-
/**
|
|
8339
|
-
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
8340
|
-
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
8341
|
-
* tRPC layer can pass it through without reshaping.
|
|
8342
|
-
*/
|
|
8343
|
-
var DeviceSummarySchema = z.object({
|
|
8344
|
-
id: z.number(),
|
|
8345
|
-
stableId: z.string(),
|
|
8346
|
-
addonId: z.string(),
|
|
8347
|
-
type: z.string(),
|
|
8348
|
-
name: z.string(),
|
|
8349
|
-
parentDeviceId: z.number().nullable(),
|
|
8350
|
-
online: z.boolean(),
|
|
8351
|
-
features: z.array(z.string()),
|
|
8352
|
-
config: z.record(z.string(), z.unknown()),
|
|
8353
|
-
/** Optional upstream-system identity (dispatch key + system tag).
|
|
8354
|
-
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
8355
|
-
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
8356
|
-
* synthetic backfill is in effect. */
|
|
8357
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
8358
|
-
});
|
|
8359
|
-
/**
|
|
8360
|
-
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
8361
|
-
* creation). Matches the UI-side `FieldProbeResult` in
|
|
8362
|
-
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
8363
|
-
* returned `labels` as chips next to the input.
|
|
8364
|
-
*/
|
|
8365
|
-
var FieldProbeResultSchema = z.object({
|
|
8366
|
-
status: z.enum(["ok", "error"]),
|
|
8367
|
-
labels: z.array(z.string()).optional(),
|
|
8368
|
-
error: z.string().optional()
|
|
8369
|
-
});
|
|
8370
|
-
/**
|
|
8371
|
-
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
8372
|
-
* it as `unknown` at the capability layer — the router just passes it
|
|
8373
|
-
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
8374
|
-
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
8375
|
-
* but we deliberately avoid a Zod mirror because the union is large and
|
|
8376
|
-
* not meant for runtime validation at this seam.
|
|
8377
|
-
*/
|
|
8378
|
-
var CreationSchemaOutputSchema = z.unknown();
|
|
8379
|
-
var deviceProviderCapability = {
|
|
8380
|
-
name: "device-provider",
|
|
8381
|
-
scope: "system",
|
|
8382
|
-
mode: "collection",
|
|
8383
|
-
methods: {
|
|
8384
|
-
start: method(z.void(), z.void(), { kind: "mutation" }),
|
|
8385
|
-
stop: method(z.void(), z.void(), { kind: "mutation" }),
|
|
8386
|
-
getStatus: method(z.void(), ProviderStatusSchema),
|
|
8387
|
-
getDevices: method(z.void(), z.array(z.object({
|
|
8388
|
-
id: z.string(),
|
|
8389
|
-
name: z.string(),
|
|
8390
|
-
type: z.string()
|
|
8391
|
-
}))),
|
|
8392
|
-
supportsDiscovery: method(z.object({}), z.boolean()),
|
|
8393
|
-
/**
|
|
8394
|
-
* Run a network scan. `params` carries optional provider-specific scan
|
|
8395
|
-
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
8396
|
-
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
8397
|
-
* (provider uses its local-network default).
|
|
8398
|
-
*/
|
|
8399
|
-
discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
|
|
8400
|
-
kind: "mutation",
|
|
8401
|
-
auth: "admin"
|
|
8402
|
-
}),
|
|
8403
|
-
/**
|
|
8404
|
-
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
8405
|
-
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
8406
|
-
* `null` when the provider takes no extra scan params — the generic
|
|
8407
|
-
* aggregated scan never renders this; the per-integration scan does.
|
|
8408
|
-
*/
|
|
8409
|
-
getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
|
|
8410
|
-
/**
|
|
8411
|
-
* The DeviceType this provider creates via manual add (Camera for
|
|
8412
|
-
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
8413
|
-
* provider does not support manual creation. Lets the Add-Device dialog
|
|
8414
|
-
* pick the right type instead of assuming Camera.
|
|
8415
|
-
*/
|
|
8416
|
-
getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
|
|
8417
|
-
adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
8418
|
-
kind: "mutation",
|
|
8419
|
-
auth: "admin"
|
|
8420
|
-
}),
|
|
8421
|
-
supportsManualCreation: method(z.object({}), z.boolean()),
|
|
8422
|
-
/**
|
|
8423
|
-
* Fetch the creation form schema for a given DeviceType. Returns
|
|
8424
|
-
* `null` when the provider does not support manually creating
|
|
8425
|
-
* devices of that type. The output is a `ConfigUISchema` — the
|
|
8426
|
-
* router type-asserts it at the boundary.
|
|
8427
|
-
*/
|
|
8428
|
-
getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
8429
|
-
createDevice: method(z.object({
|
|
8430
|
-
type: z.enum(DeviceType),
|
|
8431
|
-
config: z.record(z.string(), z.unknown())
|
|
8432
|
-
}), DeviceSummarySchema, {
|
|
8433
|
-
kind: "mutation",
|
|
8434
|
-
auth: "admin"
|
|
8435
|
-
}),
|
|
8436
|
-
/**
|
|
8437
|
-
* Test a single field in the creation form before the device has
|
|
8438
|
-
* been persisted. Typical use: probing an RTSP URL entered by the
|
|
8439
|
-
* user. Providers that don't support field probing return
|
|
8440
|
-
* `{ success: true, message: 'Field test not supported' }`.
|
|
8441
|
-
*
|
|
8442
|
-
* `formValues` is the live snapshot of every field in the form at
|
|
8443
|
-
* the moment the user clicked Test — useful for probes that depend
|
|
8444
|
-
* on multiple fields together (e.g. Reolink autodetect needs host
|
|
8445
|
-
* + credentials + UID + transport mode in a single call). Optional
|
|
8446
|
-
* for backwards compatibility; providers free to ignore it.
|
|
8447
|
-
*/
|
|
8448
|
-
testCreationField: method(z.object({
|
|
8449
|
-
type: z.enum(DeviceType),
|
|
8450
|
-
key: z.string(),
|
|
8451
|
-
value: z.unknown(),
|
|
8452
|
-
formValues: z.record(z.string(), z.unknown()).optional()
|
|
8453
|
-
}), FieldProbeResultSchema, {
|
|
8454
|
-
kind: "mutation",
|
|
8455
|
-
auth: "admin"
|
|
8456
|
-
})
|
|
8457
|
-
}
|
|
8458
|
-
};
|
|
8459
|
-
/**
|
|
8460
|
-
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
8461
|
-
* live registry access, and all management operations into a single tRPC surface.
|
|
7908
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
7909
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
8462
7910
|
*
|
|
8463
7911
|
* Replaces:
|
|
8464
7912
|
* - `device-persistence` capability (persistence methods absorbed here)
|
|
@@ -8485,92 +7933,6 @@ var ChildLayoutEntrySchema = z.object({
|
|
|
8485
7933
|
order: z.number().optional(),
|
|
8486
7934
|
collapsed: z.boolean().optional()
|
|
8487
7935
|
});
|
|
8488
|
-
/** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
|
|
8489
|
-
* `device-management.ts`. Source is a union: a FIELD source copies a sibling
|
|
8490
|
-
* accessory's status field (`kind` optional/absent for wire compat); a
|
|
8491
|
-
* LITERAL source carries a per-device constant (no sibling is read); a
|
|
8492
|
-
* GLOBAL source (P2e) copies ANY device's status field, addressed by the
|
|
8493
|
-
* source device's full re-sync-stable `stableId`. */
|
|
8494
|
-
var DeviceLinkFieldSourceSchema = z.object({
|
|
8495
|
-
kind: z.literal("field").optional(),
|
|
8496
|
-
sourceKey: z.string(),
|
|
8497
|
-
cap: z.string(),
|
|
8498
|
-
fieldPath: z.string()
|
|
8499
|
-
});
|
|
8500
|
-
var DeviceLinkLiteralSourceSchema = z.object({
|
|
8501
|
-
kind: z.literal("literal"),
|
|
8502
|
-
value: z.union([
|
|
8503
|
-
z.string(),
|
|
8504
|
-
z.number(),
|
|
8505
|
-
z.boolean(),
|
|
8506
|
-
z.null()
|
|
8507
|
-
])
|
|
8508
|
-
});
|
|
8509
|
-
var DeviceLinkGlobalSourceSchema = z.object({
|
|
8510
|
-
kind: z.literal("global"),
|
|
8511
|
-
sourceStableId: z.string(),
|
|
8512
|
-
cap: z.string(),
|
|
8513
|
-
fieldPath: z.string()
|
|
8514
|
-
});
|
|
8515
|
-
/** Expression source (Stage X): compute the target field from N named bindings
|
|
8516
|
-
* via the safe expression engine. Bindings are field | literal | global — never
|
|
8517
|
-
* another expression (no nesting). The `superRefine` runs the SAME author-time
|
|
8518
|
-
* validation as `validateExpressionSource` (compiles the expr, checks binding
|
|
8519
|
-
* names + identifier coverage) so every wire boundary that parses a DeviceLink
|
|
8520
|
-
* (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
|
|
8521
|
-
* Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
|
|
8522
|
-
var DeviceLinkExpressionSourceSchema = z.object({
|
|
8523
|
-
kind: z.literal("expression"),
|
|
8524
|
-
expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
8525
|
-
bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), z.union([
|
|
8526
|
-
DeviceLinkFieldSourceSchema,
|
|
8527
|
-
DeviceLinkLiteralSourceSchema,
|
|
8528
|
-
DeviceLinkGlobalSourceSchema
|
|
8529
|
-
]))
|
|
8530
|
-
}).superRefine((src, ctx) => {
|
|
8531
|
-
const err = validateExpressionSource(src);
|
|
8532
|
-
if (err !== null) ctx.addIssue({
|
|
8533
|
-
code: "custom",
|
|
8534
|
-
message: err,
|
|
8535
|
-
path: ["expr"]
|
|
8536
|
-
});
|
|
8537
|
-
});
|
|
8538
|
-
var DeviceLinkSchema = z.object({
|
|
8539
|
-
id: z.string(),
|
|
8540
|
-
source: z.union([
|
|
8541
|
-
DeviceLinkFieldSourceSchema,
|
|
8542
|
-
DeviceLinkLiteralSourceSchema,
|
|
8543
|
-
DeviceLinkGlobalSourceSchema,
|
|
8544
|
-
DeviceLinkExpressionSourceSchema
|
|
8545
|
-
]),
|
|
8546
|
-
target: z.object({
|
|
8547
|
-
cap: z.string(),
|
|
8548
|
-
fieldPath: z.string(),
|
|
8549
|
-
itemKey: z.string().optional()
|
|
8550
|
-
}),
|
|
8551
|
-
transform: z.discriminatedUnion("kind", [
|
|
8552
|
-
z.object({ kind: z.literal("identity") }),
|
|
8553
|
-
z.object({
|
|
8554
|
-
kind: z.literal("enum-map"),
|
|
8555
|
-
mapping: z.record(z.string(), z.union([
|
|
8556
|
-
z.string(),
|
|
8557
|
-
z.number(),
|
|
8558
|
-
z.boolean()
|
|
8559
|
-
])),
|
|
8560
|
-
fallback: z.union([
|
|
8561
|
-
z.string(),
|
|
8562
|
-
z.number(),
|
|
8563
|
-
z.boolean()
|
|
8564
|
-
]).optional()
|
|
8565
|
-
}),
|
|
8566
|
-
z.object({
|
|
8567
|
-
kind: z.literal("linear"),
|
|
8568
|
-
scale: z.number(),
|
|
8569
|
-
offset: z.number(),
|
|
8570
|
-
clamp: z.tuple([z.number(), z.number()]).readonly().optional()
|
|
8571
|
-
})
|
|
8572
|
-
]).optional()
|
|
8573
|
-
});
|
|
8574
7936
|
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
8575
7937
|
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
8576
7938
|
var DeviceCapDisplayOverrideSchema = z.object({
|
|
@@ -8650,8 +8012,6 @@ var DeviceInfoSchema = z.object({
|
|
|
8650
8012
|
* named accordion sections (with optional intra-section order). See
|
|
8651
8013
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
8652
8014
|
childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
|
|
8653
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
8654
|
-
deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
|
|
8655
8015
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
8656
8016
|
display: DeviceDisplayOverrideSchema.optional()
|
|
8657
8017
|
});
|
|
@@ -8660,7 +8020,7 @@ var ConfigEntrySchema = z.object({
|
|
|
8660
8020
|
value: z.unknown(),
|
|
8661
8021
|
description: z.string().optional()
|
|
8662
8022
|
});
|
|
8663
|
-
var
|
|
8023
|
+
var LinkedDevicesModeSchema = z.enum(["auto", "manual"]);
|
|
8664
8024
|
/** One resolved linked device — the compact projection consumers need. */
|
|
8665
8025
|
var LinkedDeviceSchema = z.object({
|
|
8666
8026
|
deviceId: z.number(),
|
|
@@ -8723,8 +8083,6 @@ var DeviceMetaSchema = z.object({
|
|
|
8723
8083
|
* accordion sections (with optional intra-section order). See
|
|
8724
8084
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
8725
8085
|
childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
|
|
8726
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
8727
|
-
deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
|
|
8728
8086
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
8729
8087
|
* Optional: only present for accessory children that carry a known role. */
|
|
8730
8088
|
role: z.string().nullable().optional(),
|
|
@@ -8876,16 +8234,6 @@ var deviceManagerCapability = {
|
|
|
8876
8234
|
kind: "mutation",
|
|
8877
8235
|
auth: "admin"
|
|
8878
8236
|
}),
|
|
8879
|
-
/** Set (or replace) the cross-device field wirings on a device's meta row.
|
|
8880
|
-
* Mirrors `setChildLayout`; persisted, projected, preserved across
|
|
8881
|
-
* re-register/restore. Idempotent. */
|
|
8882
|
-
setDeviceLinks: method(z.object({
|
|
8883
|
-
deviceId: z.number(),
|
|
8884
|
-
deviceLinks: z.array(DeviceLinkSchema).readonly()
|
|
8885
|
-
}), z.void(), {
|
|
8886
|
-
kind: "mutation",
|
|
8887
|
-
auth: "admin"
|
|
8888
|
-
}),
|
|
8889
8237
|
/** Set (or clear) the per-device display override on the meta row. Mirrors
|
|
8890
8238
|
* `setChildLayout` persistence; `null` clears the override entirely. The
|
|
8891
8239
|
* override unit(s) are normalized (`normalizeUnit`) at write so the render
|
|
@@ -8909,15 +8257,15 @@ var deviceManagerCapability = {
|
|
|
8909
8257
|
kind: "mutation",
|
|
8910
8258
|
auth: "admin"
|
|
8911
8259
|
}),
|
|
8912
|
-
/**
|
|
8913
|
-
*
|
|
8914
|
-
*
|
|
8915
|
-
*
|
|
8916
|
-
*
|
|
8917
|
-
*
|
|
8918
|
-
*
|
|
8919
|
-
*
|
|
8920
|
-
*
|
|
8260
|
+
/** The per-cap catalog of readable status-schema fields for one device.
|
|
8261
|
+
* This is the SOURCE PICKER — it outlived the Wiring tab that first
|
|
8262
|
+
* needed it (deleted 2026-08-08) and becomes the picker of a composition
|
|
8263
|
+
* recipe: every field the old tab could target, a composed device can
|
|
8264
|
+
* read. Caps without a status schema are omitted. Item-array caps
|
|
8265
|
+
* (`status.itemArray`, e.g. consumables) also emit their per-item fields
|
|
8266
|
+
* tagged `item: true` plus the cap-level `itemArray` descriptor.
|
|
8267
|
+
* `includeSynthesizable: true` unions in unbound device-scoped caps that
|
|
8268
|
+
* declare `status.empty` and match the device's type. */
|
|
8921
8269
|
getWireableFields: method(z.object({
|
|
8922
8270
|
deviceId: z.number(),
|
|
8923
8271
|
includeSynthesizable: z.boolean().optional()
|
|
@@ -9059,7 +8407,7 @@ var deviceManagerCapability = {
|
|
|
9059
8407
|
* (see the module docblock) — there are no bespoke link mutations.
|
|
9060
8408
|
*/
|
|
9061
8409
|
getLinkedDevices: method(z.object({ deviceId: z.number() }), z.object({
|
|
9062
|
-
mode:
|
|
8410
|
+
mode: LinkedDevicesModeSchema,
|
|
9063
8411
|
devices: z.array(LinkedDeviceSchema)
|
|
9064
8412
|
})),
|
|
9065
8413
|
/** Get stream sources for a camera device. */
|
|
@@ -9123,11 +8471,7 @@ var deviceManagerCapability = {
|
|
|
9123
8471
|
deviceId: z.number(),
|
|
9124
8472
|
entries: z.array(z.object({
|
|
9125
8473
|
capName: z.string(),
|
|
9126
|
-
kind: z.enum([
|
|
9127
|
-
"native",
|
|
9128
|
-
"wrapped",
|
|
9129
|
-
"linked"
|
|
9130
|
-
]),
|
|
8474
|
+
kind: z.enum(["native", "wrapped"]),
|
|
9131
8475
|
providerAddonId: z.string(),
|
|
9132
8476
|
providerNodeId: z.string(),
|
|
9133
8477
|
nativeAddonId: z.string()
|
|
@@ -9145,11 +8489,7 @@ var deviceManagerCapability = {
|
|
|
9145
8489
|
deviceId: z.number(),
|
|
9146
8490
|
entries: z.array(z.object({
|
|
9147
8491
|
capName: z.string(),
|
|
9148
|
-
kind: z.enum([
|
|
9149
|
-
"native",
|
|
9150
|
-
"wrapped",
|
|
9151
|
-
"linked"
|
|
9152
|
-
]),
|
|
8492
|
+
kind: z.enum(["native", "wrapped"]),
|
|
9153
8493
|
providerAddonId: z.string(),
|
|
9154
8494
|
providerNodeId: z.string(),
|
|
9155
8495
|
nativeAddonId: z.string()
|
|
@@ -10357,6 +9697,10 @@ var motionDetectionCapability = {
|
|
|
10357
9697
|
mode: "singleton",
|
|
10358
9698
|
kind: "wrapper",
|
|
10359
9699
|
defaultActive: true,
|
|
9700
|
+
/** Frame-differencing motion analysis over a video stream — only a camera
|
|
9701
|
+
* produces the frames. Read by the `defaultActive` auto-bind in
|
|
9702
|
+
* `device-manager.getBindings` to decide which devices it may claim. */
|
|
9703
|
+
deviceTypes: [DeviceType.Camera],
|
|
10360
9704
|
exposesDeviceSettings: true,
|
|
10361
9705
|
methods: {
|
|
10362
9706
|
analyze: method(z.object({
|
|
@@ -12750,6 +12094,18 @@ var OauthIntegrationDescriptorSchema = z.object({
|
|
|
12750
12094
|
* redirect_uri that does not start with one of these. Required —
|
|
12751
12095
|
* an empty list means the integration can never complete linking. */
|
|
12752
12096
|
allowedRedirectPrefixes: z.array(z.string()).min(1),
|
|
12097
|
+
/** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
|
|
12098
|
+
* RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
|
|
12099
|
+
* `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
|
|
12100
|
+
* whose address the hub cannot know in advance (a Home Assistant at
|
|
12101
|
+
* `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
|
|
12102
|
+
* exactly; a public host never satisfies this branch, so it is not a
|
|
12103
|
+
* wildcard prefix by another name. */
|
|
12104
|
+
allowedPrivateHostPaths: z.array(z.string()).optional(),
|
|
12105
|
+
/** When true this is a PUBLIC client (source is published, no secret can be
|
|
12106
|
+
* protected) and PKCE is mandatory: `/authorize` refuses without an S256
|
|
12107
|
+
* `code_challenge`, `/token` refuses without the matching `code_verifier`. */
|
|
12108
|
+
requiresPkce: z.boolean().optional(),
|
|
12753
12109
|
/** Optional public origin (no trailing slash) that this integration's
|
|
12754
12110
|
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
12755
12111
|
* operator-selected external-access endpoint resolved by the addon. When
|
|
@@ -12906,7 +12262,7 @@ var TrackEnvelopeSchema = z.object({
|
|
|
12906
12262
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
12907
12263
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
12908
12264
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
12909
|
-
* zonesVisited, bestEventId, envelope) and returns `positions` /
|
|
12265
|
+
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
12910
12266
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
12911
12267
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
12912
12268
|
* (`getObjectEvents` et al.).
|
|
@@ -13143,6 +12499,24 @@ var TrackSchema = z.object({
|
|
|
13143
12499
|
* Populated from the persisted envelope columns on historical reads;
|
|
13144
12500
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
13145
12501
|
envelope: TrackEnvelopeSchema.optional(),
|
|
12502
|
+
/**
|
|
12503
|
+
* A face DETECTOR found a face on this track — nothing more. It says the
|
|
12504
|
+
* detail plane produced a `face` detail; it does NOT say the face was
|
|
12505
|
+
* embedded, matched, above `minFacePx`, or that the recognizer was even
|
|
12506
|
+
* enabled. Set once and never cleared.
|
|
12507
|
+
*
|
|
12508
|
+
* **This exists so "face present but not recognised" is expressible.** A
|
|
12509
|
+
* recognised identity lands in `subLabel` (attributed to the face chain via
|
|
12510
|
+
* `subLabelMeta.stepId`), so before this field a track with an unmatched face
|
|
12511
|
+
* and a track with no face at all were byte-identical on the wire and no
|
|
12512
|
+
* surface could tell them apart. The read is `hasFace === true && subLabel
|
|
12513
|
+
* === undefined`.
|
|
12514
|
+
*
|
|
12515
|
+
* **Absent ≠ false.** Every row written before the column existed omits it,
|
|
12516
|
+
* and so does every server that predates the field — a consumer must test
|
|
12517
|
+
* `=== true` and render nothing otherwise, never infer "no face".
|
|
12518
|
+
*/
|
|
12519
|
+
hasFace: z.boolean().optional(),
|
|
13146
12520
|
...TrackFlagFields,
|
|
13147
12521
|
...TrackRetrainFields
|
|
13148
12522
|
});
|
|
@@ -16892,6 +16266,10 @@ var SsoBridgeClaimsSchema = z.object({
|
|
|
16892
16266
|
integrationId: z.string().optional(),
|
|
16893
16267
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
16894
16268
|
jti: z.string().optional(),
|
|
16269
|
+
/** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
|
|
16270
|
+
* client. Its PRESENCE is what makes the verifier mandatory at exchange,
|
|
16271
|
+
* so the requirement travels with the code and not with mutable config. */
|
|
16272
|
+
codeChallenge: z.string().optional(),
|
|
16895
16273
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
16896
16274
|
* tokens so the verify path can check the session is not revoked. */
|
|
16897
16275
|
sessionId: z.string().optional()
|
|
@@ -17648,6 +17026,10 @@ var videoclipsCapability = {
|
|
|
17648
17026
|
mode: "singleton",
|
|
17649
17027
|
kind: "wrapper",
|
|
17650
17028
|
defaultActive: true,
|
|
17029
|
+
/** A clip is a window over a camera's footage — the cap is meaningless on a
|
|
17030
|
+
* sensor, a button or an event emitter, and the `defaultActive` auto-bind
|
|
17031
|
+
* reads this to decide which devices it may claim. */
|
|
17032
|
+
deviceTypes: [DeviceType.Camera],
|
|
17651
17033
|
methods: {
|
|
17652
17034
|
listClips: method(z.object({
|
|
17653
17035
|
deviceId: z.number(),
|
|
@@ -25574,14 +24956,19 @@ var userManagementCapability = {
|
|
|
25574
24956
|
username: z.string(),
|
|
25575
24957
|
scopes: z.array(TokenScopeSchema),
|
|
25576
24958
|
redirectUri: z.string(),
|
|
25577
|
-
hubUrl: z.string()
|
|
24959
|
+
hubUrl: z.string(),
|
|
24960
|
+
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
24961
|
+
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
24962
|
+
codeChallenge: z.string().optional()
|
|
25578
24963
|
}), z.object({ code: z.string() }), {
|
|
25579
24964
|
kind: "mutation",
|
|
25580
24965
|
access: "create"
|
|
25581
24966
|
}),
|
|
25582
24967
|
oauthExchangeCode: method(z.object({
|
|
25583
24968
|
code: z.string(),
|
|
25584
|
-
redirectUri: z.string()
|
|
24969
|
+
redirectUri: z.string(),
|
|
24970
|
+
/** PKCE verifier. REQUIRED when the code carries a challenge. */
|
|
24971
|
+
codeVerifier: z.string().optional()
|
|
25585
24972
|
}), z.object({
|
|
25586
24973
|
accessToken: z.string(),
|
|
25587
24974
|
refreshToken: z.string(),
|
|
@@ -26123,277 +25510,872 @@ var zoneAnalyticsCapability = {
|
|
|
26123
25510
|
runtimeState: CameraOccupancySnapshotSchema
|
|
26124
25511
|
};
|
|
26125
25512
|
/**
|
|
26126
|
-
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
26127
|
-
* cap so a single CRUD surface backs every consumer; each stage has
|
|
26128
|
-
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
26129
|
-
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
25513
|
+
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
25514
|
+
* cap so a single CRUD surface backs every consumer; each stage has
|
|
25515
|
+
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
25516
|
+
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
25517
|
+
*
|
|
25518
|
+
* Extend the enum here when a new gating consumer comes online (audio
|
|
25519
|
+
* gating, alert filtering, …) — no other surface needs to change.
|
|
25520
|
+
*/
|
|
25521
|
+
var ZoneRuleStageEnum = z.enum([
|
|
25522
|
+
"motion",
|
|
25523
|
+
"detection",
|
|
25524
|
+
"package"
|
|
25525
|
+
]);
|
|
25526
|
+
/**
|
|
25527
|
+
* Zone rules capability — per-camera CRUD over the {@link ZoneRule}
|
|
25528
|
+
* arrays that decide how each pipeline stage uses the polygon zones.
|
|
25529
|
+
*
|
|
25530
|
+
* Hosted by `addon-pipeline-orchestrator` alongside the zones provider
|
|
25531
|
+
* so the operator has a single hub-side source of truth for both
|
|
25532
|
+
* geometry and behaviour. Per-stage rules are stored under the
|
|
25533
|
+
* `zoneRules.<stage>` key in the orchestrator's per-device store and
|
|
25534
|
+
* mirrored to the device-state slice `<stage>-zone-rules` on every
|
|
25535
|
+
* mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
|
|
25536
|
+
* subscribe to that slice and refresh their gating without
|
|
25537
|
+
* round-tripping the cap.
|
|
25538
|
+
*
|
|
25539
|
+
* Sets are bulk-replace — the operator UI sends the new rule list
|
|
25540
|
+
* wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
|
|
25541
|
+
* naturally in the rule editor without per-rule mutation chatter.
|
|
25542
|
+
*/
|
|
25543
|
+
var zoneRulesCapability = {
|
|
25544
|
+
name: "zone-rules",
|
|
25545
|
+
scope: "device",
|
|
25546
|
+
mode: "singleton",
|
|
25547
|
+
deviceTypes: [DeviceType.Camera],
|
|
25548
|
+
methods: {
|
|
25549
|
+
/** Read the full rule list for a given stage (empty when no rules
|
|
25550
|
+
* are defined yet). */
|
|
25551
|
+
listRules: method(z.object({
|
|
25552
|
+
deviceId: z.number(),
|
|
25553
|
+
stage: ZoneRuleStageEnum
|
|
25554
|
+
}), z.array(ZoneRuleSchema).readonly()),
|
|
25555
|
+
/** Bulk-replace the rule list for one stage. The provider validates
|
|
25556
|
+
* each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
|
|
25557
|
+
* thresholds in range) and rejects the whole patch if any entry
|
|
25558
|
+
* is invalid — partial writes are a configuration footgun. */
|
|
25559
|
+
setRules: method(z.object({
|
|
25560
|
+
deviceId: z.number(),
|
|
25561
|
+
stage: ZoneRuleStageEnum,
|
|
25562
|
+
rules: z.array(ZoneRuleSchema).readonly()
|
|
25563
|
+
}), z.void(), {
|
|
25564
|
+
kind: "mutation",
|
|
25565
|
+
auth: "admin"
|
|
25566
|
+
})
|
|
25567
|
+
},
|
|
25568
|
+
/**
|
|
25569
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
25570
|
+
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
25571
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
25572
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
25573
|
+
* complete current set. Consumers that only care about one stage
|
|
25574
|
+
* just read the matching property.
|
|
25575
|
+
*
|
|
25576
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
25577
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
25578
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
25579
|
+
* The orchestrator provider writes this stage as a first-class slice
|
|
25580
|
+
* (Phase 4): every mutation mirrors the full `{motion, detection,
|
|
25581
|
+
* package}` shape, so consumers read the current package rules directly
|
|
25582
|
+
* off `device.state.zoneRules.value.package`.
|
|
25583
|
+
*/
|
|
25584
|
+
runtimeState: z.object({
|
|
25585
|
+
motion: z.array(ZoneRuleSchema).readonly(),
|
|
25586
|
+
detection: z.array(ZoneRuleSchema).readonly(),
|
|
25587
|
+
package: z.array(ZoneRuleSchema).readonly()
|
|
25588
|
+
})
|
|
25589
|
+
};
|
|
25590
|
+
/**
|
|
25591
|
+
* Runtime defaults -- used by ConfigManager.get() for backward compatibility
|
|
25592
|
+
* until Plan B wires all runtime settings to the system_settings SQL table.
|
|
25593
|
+
*
|
|
25594
|
+
* Moved from @camstack/system/config-schema to @camstack/types so that
|
|
25595
|
+
* @camstack/system can reference it without a cross-package kernel import.
|
|
25596
|
+
*/
|
|
25597
|
+
var RUNTIME_DEFAULTS = {
|
|
25598
|
+
"features.streaming": true,
|
|
25599
|
+
"features.notifications": true,
|
|
25600
|
+
"features.objectDetection": false,
|
|
25601
|
+
"features.remoteAccess": true,
|
|
25602
|
+
"features.agentCluster": false,
|
|
25603
|
+
"features.smartHome": true,
|
|
25604
|
+
"features.recordings": true,
|
|
25605
|
+
"features.backup": true,
|
|
25606
|
+
"features.repl": true,
|
|
25607
|
+
"retention.detectionEventsDays": 30,
|
|
25608
|
+
"retention.audioLevelsDays": 7,
|
|
25609
|
+
"logging.level": "info",
|
|
25610
|
+
"logging.retentionDays": 30,
|
|
25611
|
+
"eventBus.ringBufferSize": 1e4,
|
|
25612
|
+
"storage.provider": "sqlite-storage",
|
|
25613
|
+
"storage.locations": {
|
|
25614
|
+
data: "camstack-data/data",
|
|
25615
|
+
media: "camstack-data/media",
|
|
25616
|
+
recordings: "camstack-data/recordings",
|
|
25617
|
+
cache: "/tmp/camstack-cache",
|
|
25618
|
+
logs: "camstack-data/logs",
|
|
25619
|
+
models: "camstack-data/models"
|
|
25620
|
+
},
|
|
25621
|
+
providers: [],
|
|
25622
|
+
"ffmpeg.binaryPath": "ffmpeg",
|
|
25623
|
+
"ffmpeg.hwAccel": "auto",
|
|
25624
|
+
"ffmpeg.threadCount": 0,
|
|
25625
|
+
"auth.tokenExpiry": "7d"
|
|
25626
|
+
};
|
|
25627
|
+
/**
|
|
25628
|
+
* Accessory device helpers — shared across drivers.
|
|
26130
25629
|
*
|
|
26131
|
-
*
|
|
26132
|
-
*
|
|
25630
|
+
* Many vendor-specific drivers register accessory child devices on
|
|
25631
|
+
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
25632
|
+
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
25633
|
+
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
25634
|
+
* spawning, builds a name derived from the parent, and produces a
|
|
25635
|
+
* stableId tied to the parent so boot-restore can reconstruct the
|
|
25636
|
+
* relationship.
|
|
25637
|
+
*
|
|
25638
|
+
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
25639
|
+
* drivers may reasonably disagree on the right type for an accessory
|
|
25640
|
+
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
25641
|
+
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
25642
|
+
* one canonical mapping was over-prescriptive and added a layer of
|
|
25643
|
+
* indirection without saving meaningful code at call sites — the
|
|
25644
|
+
* driver knows its own hardware best.
|
|
26133
25645
|
*/
|
|
26134
|
-
var ZoneRuleStageEnum = z.enum([
|
|
26135
|
-
"motion",
|
|
26136
|
-
"detection",
|
|
26137
|
-
"package"
|
|
26138
|
-
]);
|
|
26139
25646
|
/**
|
|
26140
|
-
*
|
|
26141
|
-
*
|
|
25647
|
+
* Subset of `DeviceRole` values that drivers register as child
|
|
25648
|
+
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
25649
|
+
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
25650
|
+
* children, so the call site reads as
|
|
25651
|
+
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
25652
|
+
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
25653
|
+
* any role works, including non-accessory ones like Doorbell).
|
|
25654
|
+
*/
|
|
25655
|
+
var AccessoryKind = {
|
|
25656
|
+
Siren: DeviceRole.Siren,
|
|
25657
|
+
Floodlight: DeviceRole.Floodlight,
|
|
25658
|
+
Spotlight: DeviceRole.Spotlight,
|
|
25659
|
+
PirSensor: DeviceRole.PirSensor,
|
|
25660
|
+
Chime: DeviceRole.Chime,
|
|
25661
|
+
Autotrack: DeviceRole.Autotrack,
|
|
25662
|
+
Nightvision: DeviceRole.Nightvision,
|
|
25663
|
+
PrivacyMask: DeviceRole.PrivacyMask
|
|
25664
|
+
};
|
|
25665
|
+
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
25666
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
25667
|
+
new Set(Object.values(DeviceType));
|
|
25668
|
+
DeviceFeature.BatteryOperated;
|
|
25669
|
+
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
|
|
25670
|
+
* Zod v4 exposes `.unwrap()` on all three wrapper classes. */
|
|
25671
|
+
function unwrap(schema) {
|
|
25672
|
+
let s = schema;
|
|
25673
|
+
while (s instanceof z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
|
|
25674
|
+
return s;
|
|
25675
|
+
}
|
|
25676
|
+
function leafKind(schema) {
|
|
25677
|
+
const s = unwrap(schema);
|
|
25678
|
+
if (s instanceof z.ZodEnum) return {
|
|
25679
|
+
kind: "enum",
|
|
25680
|
+
enumValues: s.options
|
|
25681
|
+
};
|
|
25682
|
+
if (s instanceof z.ZodNumber) return { kind: "number" };
|
|
25683
|
+
if (s instanceof z.ZodString) return { kind: "string" };
|
|
25684
|
+
if (s instanceof z.ZodBoolean) return { kind: "boolean" };
|
|
25685
|
+
return null;
|
|
25686
|
+
}
|
|
25687
|
+
/** Walk a cap status schema into flat wireable leaf fields (dotted paths).
|
|
25688
|
+
* Recurses into nested ZodObject (unwrapping nullable/optional/default first),
|
|
25689
|
+
* so fields with null live values are still offered. Arrays / unknown shapes
|
|
25690
|
+
* are skipped — never throws. */
|
|
25691
|
+
function enumerateSchemaFields(schema, prefix = "") {
|
|
25692
|
+
const root = unwrap(schema);
|
|
25693
|
+
if (!(root instanceof z.ZodObject)) return [];
|
|
25694
|
+
const out = [];
|
|
25695
|
+
const shape = root.shape;
|
|
25696
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
25697
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
25698
|
+
const inner = unwrap(field);
|
|
25699
|
+
if (inner instanceof z.ZodObject) {
|
|
25700
|
+
out.push(...enumerateSchemaFields(inner, path));
|
|
25701
|
+
continue;
|
|
25702
|
+
}
|
|
25703
|
+
const leaf = leafKind(field);
|
|
25704
|
+
if (leaf) out.push({
|
|
25705
|
+
path,
|
|
25706
|
+
kind: leaf.kind,
|
|
25707
|
+
...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
|
|
25708
|
+
});
|
|
25709
|
+
}
|
|
25710
|
+
return out;
|
|
25711
|
+
}
|
|
25712
|
+
/** Enumerate the per-item wireable fields of an item-array cap (see
|
|
25713
|
+
* `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
|
|
25714
|
+
* `keyField` (the key comes from the link's `itemKey`, never from a wired
|
|
25715
|
+
* source), each tagged `item: true` so the authoring UI collects an
|
|
25716
|
+
* `itemKey` alongside the field. Never throws. */
|
|
25717
|
+
function enumerateItemArrayFields(itemArray) {
|
|
25718
|
+
return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
|
|
25719
|
+
...f,
|
|
25720
|
+
item: true
|
|
25721
|
+
}));
|
|
25722
|
+
}
|
|
25723
|
+
/**
|
|
25724
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
25725
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
25726
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
25727
|
+
*/
|
|
25728
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
25729
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
25730
|
+
var ExpressionParseError = class extends Error {
|
|
25731
|
+
position;
|
|
25732
|
+
constructor(message, position) {
|
|
25733
|
+
super(message);
|
|
25734
|
+
this.name = "ExpressionParseError";
|
|
25735
|
+
this.position = position;
|
|
25736
|
+
}
|
|
25737
|
+
};
|
|
25738
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
25739
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
25740
|
+
var ExpressionEvalError = class extends Error {
|
|
25741
|
+
constructor(message) {
|
|
25742
|
+
super(message);
|
|
25743
|
+
this.name = "ExpressionEvalError";
|
|
25744
|
+
}
|
|
25745
|
+
};
|
|
25746
|
+
/**
|
|
25747
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
25748
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
25749
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
25750
|
+
* own-property check against it.
|
|
26142
25751
|
*
|
|
26143
|
-
*
|
|
26144
|
-
*
|
|
26145
|
-
*
|
|
26146
|
-
* `
|
|
26147
|
-
*
|
|
26148
|
-
* mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
|
|
26149
|
-
* subscribe to that slice and refresh their gating without
|
|
26150
|
-
* round-tripping the cap.
|
|
25752
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
25753
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
25754
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
25755
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
25756
|
+
* callable — they are simply "unknown function" at parse time.
|
|
26151
25757
|
*
|
|
26152
|
-
*
|
|
26153
|
-
*
|
|
26154
|
-
*
|
|
25758
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
25759
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
25760
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
25761
|
+
* closed rather than emitting a garbage value.
|
|
26155
25762
|
*/
|
|
26156
|
-
|
|
26157
|
-
name:
|
|
26158
|
-
|
|
26159
|
-
|
|
26160
|
-
|
|
26161
|
-
|
|
26162
|
-
|
|
26163
|
-
|
|
26164
|
-
|
|
26165
|
-
|
|
26166
|
-
|
|
26167
|
-
|
|
26168
|
-
|
|
26169
|
-
|
|
26170
|
-
|
|
26171
|
-
|
|
26172
|
-
|
|
26173
|
-
|
|
26174
|
-
|
|
26175
|
-
|
|
26176
|
-
|
|
26177
|
-
|
|
26178
|
-
|
|
26179
|
-
|
|
25763
|
+
function asFiniteNumber(value, name, index) {
|
|
25764
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
25765
|
+
return value;
|
|
25766
|
+
}
|
|
25767
|
+
function asString$1(value, name, index) {
|
|
25768
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
25769
|
+
return value;
|
|
25770
|
+
}
|
|
25771
|
+
function finiteResult(value, name) {
|
|
25772
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
25773
|
+
return value;
|
|
25774
|
+
}
|
|
25775
|
+
function allFiniteNumbers(args, name) {
|
|
25776
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
25777
|
+
}
|
|
25778
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
25779
|
+
var table = {
|
|
25780
|
+
min: {
|
|
25781
|
+
minArgs: 1,
|
|
25782
|
+
maxArgs: INF,
|
|
25783
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
25784
|
+
},
|
|
25785
|
+
max: {
|
|
25786
|
+
minArgs: 1,
|
|
25787
|
+
maxArgs: INF,
|
|
25788
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
25789
|
+
},
|
|
25790
|
+
abs: {
|
|
25791
|
+
minArgs: 1,
|
|
25792
|
+
maxArgs: 1,
|
|
25793
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
25794
|
+
},
|
|
25795
|
+
floor: {
|
|
25796
|
+
minArgs: 1,
|
|
25797
|
+
maxArgs: 1,
|
|
25798
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
25799
|
+
},
|
|
25800
|
+
ceil: {
|
|
25801
|
+
minArgs: 1,
|
|
25802
|
+
maxArgs: 1,
|
|
25803
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
25804
|
+
},
|
|
25805
|
+
sqrt: {
|
|
25806
|
+
minArgs: 1,
|
|
25807
|
+
maxArgs: 1,
|
|
25808
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
25809
|
+
},
|
|
25810
|
+
round: {
|
|
25811
|
+
minArgs: 1,
|
|
25812
|
+
maxArgs: 2,
|
|
25813
|
+
apply: (args) => {
|
|
25814
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
25815
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
25816
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
25817
|
+
const factor = 10 ** digits;
|
|
25818
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
25819
|
+
}
|
|
25820
|
+
},
|
|
25821
|
+
pow: {
|
|
25822
|
+
minArgs: 2,
|
|
25823
|
+
maxArgs: 2,
|
|
25824
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
25825
|
+
},
|
|
25826
|
+
clamp: {
|
|
25827
|
+
minArgs: 3,
|
|
25828
|
+
maxArgs: 3,
|
|
25829
|
+
apply: (args) => {
|
|
25830
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
25831
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
25832
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
25833
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
25834
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
25835
|
+
}
|
|
25836
|
+
},
|
|
25837
|
+
avg: {
|
|
25838
|
+
minArgs: 1,
|
|
25839
|
+
maxArgs: INF,
|
|
25840
|
+
apply: (args) => {
|
|
25841
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
25842
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
25843
|
+
}
|
|
26180
25844
|
},
|
|
26181
|
-
|
|
26182
|
-
|
|
26183
|
-
|
|
26184
|
-
|
|
26185
|
-
|
|
26186
|
-
|
|
26187
|
-
|
|
26188
|
-
|
|
26189
|
-
|
|
26190
|
-
|
|
26191
|
-
|
|
26192
|
-
|
|
26193
|
-
|
|
26194
|
-
|
|
26195
|
-
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
|
|
26199
|
-
|
|
26200
|
-
|
|
26201
|
-
|
|
25845
|
+
sum: {
|
|
25846
|
+
minArgs: 1,
|
|
25847
|
+
maxArgs: INF,
|
|
25848
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
25849
|
+
},
|
|
25850
|
+
coalesce: {
|
|
25851
|
+
minArgs: 1,
|
|
25852
|
+
maxArgs: INF,
|
|
25853
|
+
apply: (args) => {
|
|
25854
|
+
for (const a of args) if (a !== null) return a;
|
|
25855
|
+
return null;
|
|
25856
|
+
}
|
|
25857
|
+
},
|
|
25858
|
+
age: {
|
|
25859
|
+
minArgs: 2,
|
|
25860
|
+
maxArgs: 2,
|
|
25861
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
25862
|
+
},
|
|
25863
|
+
convert: {
|
|
25864
|
+
minArgs: 3,
|
|
25865
|
+
maxArgs: 3,
|
|
25866
|
+
apply: (args, hooks) => {
|
|
25867
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
25868
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
25869
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
25870
|
+
if (hooks.convert) {
|
|
25871
|
+
const out = hooks.convert(x, from, to);
|
|
25872
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
25873
|
+
return finiteResult(out, "convert");
|
|
25874
|
+
}
|
|
25875
|
+
if (from === to) return x;
|
|
25876
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
25877
|
+
}
|
|
25878
|
+
}
|
|
26202
25879
|
};
|
|
25880
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
25881
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
25882
|
+
* callees at parse time (immediate author feedback). */
|
|
25883
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
26203
25884
|
/**
|
|
26204
|
-
*
|
|
26205
|
-
* until Plan B wires all runtime settings to the system_settings SQL table.
|
|
25885
|
+
* Resource-bound constants for the safe expression engine.
|
|
26206
25886
|
*
|
|
26207
|
-
*
|
|
26208
|
-
*
|
|
25887
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
25888
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
25889
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
25890
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
25891
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
26209
25892
|
*/
|
|
26210
|
-
|
|
26211
|
-
|
|
26212
|
-
|
|
26213
|
-
|
|
26214
|
-
|
|
26215
|
-
|
|
26216
|
-
|
|
26217
|
-
|
|
26218
|
-
"
|
|
26219
|
-
"
|
|
26220
|
-
"
|
|
26221
|
-
"
|
|
26222
|
-
|
|
26223
|
-
"logging.retentionDays": 30,
|
|
26224
|
-
"eventBus.ringBufferSize": 1e4,
|
|
26225
|
-
"storage.provider": "sqlite-storage",
|
|
26226
|
-
"storage.locations": {
|
|
26227
|
-
data: "camstack-data/data",
|
|
26228
|
-
media: "camstack-data/media",
|
|
26229
|
-
recordings: "camstack-data/recordings",
|
|
26230
|
-
cache: "/tmp/camstack-cache",
|
|
26231
|
-
logs: "camstack-data/logs",
|
|
26232
|
-
models: "camstack-data/models"
|
|
26233
|
-
},
|
|
26234
|
-
providers: [],
|
|
26235
|
-
"ffmpeg.binaryPath": "ffmpeg",
|
|
26236
|
-
"ffmpeg.hwAccel": "auto",
|
|
26237
|
-
"ffmpeg.threadCount": 0,
|
|
26238
|
-
"auth.tokenExpiry": "7d"
|
|
26239
|
-
};
|
|
25893
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
25894
|
+
* rejected without allocation. */
|
|
25895
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
25896
|
+
/** A legal binding / identifier name. */
|
|
25897
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25898
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
25899
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
25900
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
25901
|
+
"now",
|
|
25902
|
+
"true",
|
|
25903
|
+
"false",
|
|
25904
|
+
"null"
|
|
25905
|
+
]);
|
|
26240
25906
|
/**
|
|
26241
|
-
*
|
|
25907
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
25908
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
25909
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
25910
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
25911
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
25912
|
+
* is a parse error with a source position, so member access / assignment /
|
|
25913
|
+
* template literals are lexically impossible.
|
|
25914
|
+
*/
|
|
25915
|
+
var KEYWORDS = new Set([
|
|
25916
|
+
"true",
|
|
25917
|
+
"false",
|
|
25918
|
+
"null"
|
|
25919
|
+
]);
|
|
25920
|
+
function isDigit(ch) {
|
|
25921
|
+
return ch >= "0" && ch <= "9";
|
|
25922
|
+
}
|
|
25923
|
+
function isIdentStart(ch) {
|
|
25924
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
25925
|
+
}
|
|
25926
|
+
function isIdentPart(ch) {
|
|
25927
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
25928
|
+
}
|
|
25929
|
+
function isWhitespace(ch) {
|
|
25930
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
25931
|
+
}
|
|
25932
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
25933
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
25934
|
+
* string. */
|
|
25935
|
+
function tokenize(source) {
|
|
25936
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
25937
|
+
const tokens = [];
|
|
25938
|
+
let i = 0;
|
|
25939
|
+
const n = source.length;
|
|
25940
|
+
while (i < n) {
|
|
25941
|
+
const ch = source[i];
|
|
25942
|
+
if (isWhitespace(ch)) {
|
|
25943
|
+
i += 1;
|
|
25944
|
+
continue;
|
|
25945
|
+
}
|
|
25946
|
+
if (isDigit(ch)) {
|
|
25947
|
+
const start = i;
|
|
25948
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
25949
|
+
if (i < n && source[i] === ".") {
|
|
25950
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
25951
|
+
i += 1;
|
|
25952
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
25953
|
+
}
|
|
25954
|
+
const text = source.slice(start, i);
|
|
25955
|
+
const value = Number(text);
|
|
25956
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
25957
|
+
tokens.push({
|
|
25958
|
+
type: "number",
|
|
25959
|
+
value,
|
|
25960
|
+
pos: start
|
|
25961
|
+
});
|
|
25962
|
+
continue;
|
|
25963
|
+
}
|
|
25964
|
+
if (ch === "'" || ch === "\"") {
|
|
25965
|
+
const quote = ch;
|
|
25966
|
+
const start = i;
|
|
25967
|
+
i += 1;
|
|
25968
|
+
let out = "";
|
|
25969
|
+
let closed = false;
|
|
25970
|
+
while (i < n) {
|
|
25971
|
+
const c = source[i];
|
|
25972
|
+
if (c === "\\") {
|
|
25973
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
25974
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
25975
|
+
out += next;
|
|
25976
|
+
i += 2;
|
|
25977
|
+
continue;
|
|
25978
|
+
}
|
|
25979
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
25980
|
+
}
|
|
25981
|
+
if (c === quote) {
|
|
25982
|
+
closed = true;
|
|
25983
|
+
i += 1;
|
|
25984
|
+
break;
|
|
25985
|
+
}
|
|
25986
|
+
out += c;
|
|
25987
|
+
i += 1;
|
|
25988
|
+
}
|
|
25989
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
25990
|
+
tokens.push({
|
|
25991
|
+
type: "string",
|
|
25992
|
+
value: out,
|
|
25993
|
+
pos: start
|
|
25994
|
+
});
|
|
25995
|
+
continue;
|
|
25996
|
+
}
|
|
25997
|
+
if (isIdentStart(ch)) {
|
|
25998
|
+
const start = i;
|
|
25999
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
26000
|
+
const text = source.slice(start, i);
|
|
26001
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
26002
|
+
type: "keyword",
|
|
26003
|
+
keyword: keywordOf(text),
|
|
26004
|
+
pos: start
|
|
26005
|
+
});
|
|
26006
|
+
else tokens.push({
|
|
26007
|
+
type: "identifier",
|
|
26008
|
+
name: text,
|
|
26009
|
+
pos: start
|
|
26010
|
+
});
|
|
26011
|
+
continue;
|
|
26012
|
+
}
|
|
26013
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
26014
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
26015
|
+
tokens.push({
|
|
26016
|
+
type: "punct",
|
|
26017
|
+
punct: two,
|
|
26018
|
+
pos: i
|
|
26019
|
+
});
|
|
26020
|
+
i += 2;
|
|
26021
|
+
continue;
|
|
26022
|
+
}
|
|
26023
|
+
if (isSinglePunct(ch)) {
|
|
26024
|
+
tokens.push({
|
|
26025
|
+
type: "punct",
|
|
26026
|
+
punct: ch,
|
|
26027
|
+
pos: i
|
|
26028
|
+
});
|
|
26029
|
+
i += 1;
|
|
26030
|
+
continue;
|
|
26031
|
+
}
|
|
26032
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
26033
|
+
}
|
|
26034
|
+
tokens.push({
|
|
26035
|
+
type: "eof",
|
|
26036
|
+
pos: n
|
|
26037
|
+
});
|
|
26038
|
+
return tokens;
|
|
26039
|
+
}
|
|
26040
|
+
function keywordOf(text) {
|
|
26041
|
+
if (text === "true") return "true";
|
|
26042
|
+
if (text === "false") return "false";
|
|
26043
|
+
return "null";
|
|
26044
|
+
}
|
|
26045
|
+
function isSinglePunct(ch) {
|
|
26046
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
26047
|
+
}
|
|
26048
|
+
/**
|
|
26049
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
26242
26050
|
*
|
|
26243
|
-
*
|
|
26244
|
-
*
|
|
26245
|
-
*
|
|
26246
|
-
*
|
|
26247
|
-
*
|
|
26248
|
-
*
|
|
26249
|
-
* relationship.
|
|
26051
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
26052
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
26053
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
26054
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
26055
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
26056
|
+
* that references a since-removed builtin degrades at read.
|
|
26250
26057
|
*
|
|
26251
|
-
*
|
|
26252
|
-
*
|
|
26253
|
-
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
26254
|
-
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
26255
|
-
* one canonical mapping was over-prescriptive and added a layer of
|
|
26256
|
-
* indirection without saving meaningful code at call sites — the
|
|
26257
|
-
* driver knows its own hardware best.
|
|
26058
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
26059
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
26258
26060
|
*/
|
|
26061
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
26062
|
+
var BINARY_PRECEDENCE = {
|
|
26063
|
+
"||": 1,
|
|
26064
|
+
"&&": 2,
|
|
26065
|
+
"==": 3,
|
|
26066
|
+
"!=": 3,
|
|
26067
|
+
"<": 4,
|
|
26068
|
+
"<=": 4,
|
|
26069
|
+
">": 4,
|
|
26070
|
+
">=": 4,
|
|
26071
|
+
"+": 5,
|
|
26072
|
+
"-": 5,
|
|
26073
|
+
"*": 6,
|
|
26074
|
+
"/": 6,
|
|
26075
|
+
"%": 6
|
|
26076
|
+
};
|
|
26077
|
+
function isLogicalOp(op) {
|
|
26078
|
+
return op === "&&" || op === "||";
|
|
26079
|
+
}
|
|
26080
|
+
function isBinaryOp(op) {
|
|
26081
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
26082
|
+
}
|
|
26083
|
+
var Parser = class {
|
|
26084
|
+
tokens;
|
|
26085
|
+
pos = 0;
|
|
26086
|
+
nodeCount = 0;
|
|
26087
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
26088
|
+
callees = /* @__PURE__ */ new Set();
|
|
26089
|
+
constructor(tokens) {
|
|
26090
|
+
this.tokens = tokens;
|
|
26091
|
+
}
|
|
26092
|
+
parse() {
|
|
26093
|
+
const ast = this.parseTernary();
|
|
26094
|
+
const tok = this.peek();
|
|
26095
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
26096
|
+
return {
|
|
26097
|
+
ast,
|
|
26098
|
+
identifiers: this.identifiers,
|
|
26099
|
+
callees: this.callees,
|
|
26100
|
+
nodeCount: this.nodeCount
|
|
26101
|
+
};
|
|
26102
|
+
}
|
|
26103
|
+
peek() {
|
|
26104
|
+
return this.tokens[this.pos];
|
|
26105
|
+
}
|
|
26106
|
+
next() {
|
|
26107
|
+
return this.tokens[this.pos++];
|
|
26108
|
+
}
|
|
26109
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
26110
|
+
expectPunct(punct) {
|
|
26111
|
+
const tok = this.peek();
|
|
26112
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
26113
|
+
this.pos += 1;
|
|
26114
|
+
}
|
|
26115
|
+
matchPunct(punct) {
|
|
26116
|
+
const tok = this.peek();
|
|
26117
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
26118
|
+
this.pos += 1;
|
|
26119
|
+
return true;
|
|
26120
|
+
}
|
|
26121
|
+
return false;
|
|
26122
|
+
}
|
|
26123
|
+
countNode() {
|
|
26124
|
+
this.nodeCount += 1;
|
|
26125
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
26126
|
+
}
|
|
26127
|
+
parseTernary() {
|
|
26128
|
+
const test = this.parseBinary(1);
|
|
26129
|
+
if (this.matchPunct("?")) {
|
|
26130
|
+
const consequent = this.parseTernary();
|
|
26131
|
+
this.expectPunct(":");
|
|
26132
|
+
const alternate = this.parseTernary();
|
|
26133
|
+
this.countNode();
|
|
26134
|
+
return {
|
|
26135
|
+
kind: "conditional",
|
|
26136
|
+
test,
|
|
26137
|
+
consequent,
|
|
26138
|
+
alternate
|
|
26139
|
+
};
|
|
26140
|
+
}
|
|
26141
|
+
return test;
|
|
26142
|
+
}
|
|
26143
|
+
parseBinary(minPrec) {
|
|
26144
|
+
let left = this.parseUnary();
|
|
26145
|
+
for (;;) {
|
|
26146
|
+
const tok = this.peek();
|
|
26147
|
+
if (tok.type !== "punct") break;
|
|
26148
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
26149
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
26150
|
+
const op = tok.punct;
|
|
26151
|
+
this.pos += 1;
|
|
26152
|
+
const right = this.parseBinary(prec + 1);
|
|
26153
|
+
this.countNode();
|
|
26154
|
+
if (isLogicalOp(op)) left = {
|
|
26155
|
+
kind: "logical",
|
|
26156
|
+
op,
|
|
26157
|
+
left,
|
|
26158
|
+
right
|
|
26159
|
+
};
|
|
26160
|
+
else if (isBinaryOp(op)) left = {
|
|
26161
|
+
kind: "binary",
|
|
26162
|
+
op,
|
|
26163
|
+
left,
|
|
26164
|
+
right
|
|
26165
|
+
};
|
|
26166
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
26167
|
+
}
|
|
26168
|
+
return left;
|
|
26169
|
+
}
|
|
26170
|
+
parseUnary() {
|
|
26171
|
+
const tok = this.peek();
|
|
26172
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
26173
|
+
const op = tok.punct;
|
|
26174
|
+
this.pos += 1;
|
|
26175
|
+
const operand = this.parseUnary();
|
|
26176
|
+
this.countNode();
|
|
26177
|
+
return {
|
|
26178
|
+
kind: "unary",
|
|
26179
|
+
op,
|
|
26180
|
+
operand
|
|
26181
|
+
};
|
|
26182
|
+
}
|
|
26183
|
+
return this.parsePrimary();
|
|
26184
|
+
}
|
|
26185
|
+
parsePrimary() {
|
|
26186
|
+
const tok = this.next();
|
|
26187
|
+
switch (tok.type) {
|
|
26188
|
+
case "number":
|
|
26189
|
+
this.countNode();
|
|
26190
|
+
return {
|
|
26191
|
+
kind: "literal",
|
|
26192
|
+
value: tok.value
|
|
26193
|
+
};
|
|
26194
|
+
case "string":
|
|
26195
|
+
this.countNode();
|
|
26196
|
+
return {
|
|
26197
|
+
kind: "literal",
|
|
26198
|
+
value: tok.value
|
|
26199
|
+
};
|
|
26200
|
+
case "keyword":
|
|
26201
|
+
this.countNode();
|
|
26202
|
+
return {
|
|
26203
|
+
kind: "literal",
|
|
26204
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
26205
|
+
};
|
|
26206
|
+
case "identifier": {
|
|
26207
|
+
const nextTok = this.peek();
|
|
26208
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
26209
|
+
this.identifiers.add(tok.name);
|
|
26210
|
+
this.countNode();
|
|
26211
|
+
return {
|
|
26212
|
+
kind: "identifier",
|
|
26213
|
+
name: tok.name
|
|
26214
|
+
};
|
|
26215
|
+
}
|
|
26216
|
+
case "punct":
|
|
26217
|
+
if (tok.punct === "(") {
|
|
26218
|
+
const inner = this.parseTernary();
|
|
26219
|
+
this.expectPunct(")");
|
|
26220
|
+
return inner;
|
|
26221
|
+
}
|
|
26222
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
26223
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
26224
|
+
}
|
|
26225
|
+
}
|
|
26226
|
+
parseCall(callee, pos) {
|
|
26227
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
26228
|
+
this.expectPunct("(");
|
|
26229
|
+
const args = [];
|
|
26230
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
26231
|
+
args.push(this.parseTernary());
|
|
26232
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
26233
|
+
if (this.matchPunct(",")) continue;
|
|
26234
|
+
this.expectPunct(")");
|
|
26235
|
+
break;
|
|
26236
|
+
}
|
|
26237
|
+
this.callees.add(callee);
|
|
26238
|
+
this.countNode();
|
|
26239
|
+
return {
|
|
26240
|
+
kind: "call",
|
|
26241
|
+
callee,
|
|
26242
|
+
args
|
|
26243
|
+
};
|
|
26244
|
+
}
|
|
26245
|
+
};
|
|
26246
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
26247
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
26248
|
+
function parseExpression(source) {
|
|
26249
|
+
return new Parser(tokenize(source)).parse();
|
|
26250
|
+
}
|
|
26259
26251
|
/**
|
|
26260
|
-
*
|
|
26261
|
-
*
|
|
26262
|
-
*
|
|
26263
|
-
*
|
|
26264
|
-
*
|
|
26265
|
-
*
|
|
26266
|
-
*
|
|
26252
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
26253
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
26254
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
26255
|
+
* one per read on a hot resolve path.
|
|
26256
|
+
*
|
|
26257
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
26258
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
26259
|
+
* callers is safe and maximises hit rate.
|
|
26267
26260
|
*/
|
|
26268
|
-
var
|
|
26269
|
-
|
|
26270
|
-
|
|
26271
|
-
|
|
26272
|
-
|
|
26273
|
-
|
|
26274
|
-
|
|
26275
|
-
|
|
26276
|
-
|
|
26277
|
-
|
|
26278
|
-
|
|
26279
|
-
|
|
26280
|
-
|
|
26281
|
-
|
|
26282
|
-
|
|
26283
|
-
|
|
26284
|
-
|
|
26285
|
-
|
|
26286
|
-
|
|
26287
|
-
const key = String(value);
|
|
26288
|
-
if (Object.prototype.hasOwnProperty.call(transform.mapping, key)) return transform.mapping[key];
|
|
26289
|
-
return transform.fallback ?? null;
|
|
26261
|
+
var cache = /* @__PURE__ */ new Map();
|
|
26262
|
+
function getCached(source) {
|
|
26263
|
+
const hit = cache.get(source);
|
|
26264
|
+
if (hit !== void 0) {
|
|
26265
|
+
cache.delete(source);
|
|
26266
|
+
cache.set(source, hit);
|
|
26267
|
+
return hit;
|
|
26268
|
+
}
|
|
26269
|
+
let result;
|
|
26270
|
+
try {
|
|
26271
|
+
result = {
|
|
26272
|
+
ok: true,
|
|
26273
|
+
parsed: parseExpression(source)
|
|
26274
|
+
};
|
|
26275
|
+
} catch (err) {
|
|
26276
|
+
result = {
|
|
26277
|
+
ok: false,
|
|
26278
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
26279
|
+
};
|
|
26290
26280
|
}
|
|
26291
|
-
|
|
26292
|
-
|
|
26293
|
-
|
|
26294
|
-
if (
|
|
26295
|
-
const min = transform.clamp[0];
|
|
26296
|
-
const max = transform.clamp[1];
|
|
26297
|
-
out = Math.min(max, Math.max(min, out));
|
|
26298
|
-
}
|
|
26299
|
-
return out;
|
|
26281
|
+
cache.set(source, result);
|
|
26282
|
+
if (cache.size > 256) {
|
|
26283
|
+
const oldest = cache.keys().next().value;
|
|
26284
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
26300
26285
|
}
|
|
26301
|
-
return
|
|
26286
|
+
return result;
|
|
26302
26287
|
}
|
|
26303
|
-
|
|
26304
|
-
|
|
26305
|
-
function
|
|
26306
|
-
return
|
|
26288
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
26289
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
26290
|
+
function compileExpressionSafe(source) {
|
|
26291
|
+
return getCached(source);
|
|
26307
26292
|
}
|
|
26308
|
-
|
|
26309
|
-
|
|
26310
|
-
|
|
26311
|
-
|
|
26312
|
-
|
|
26313
|
-
|
|
26314
|
-
|
|
26315
|
-
|
|
26293
|
+
Object.freeze({});
|
|
26294
|
+
/**
|
|
26295
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
26296
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
26297
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
26298
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
26299
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
26300
|
+
*/
|
|
26301
|
+
function validateExpressionSource(src) {
|
|
26302
|
+
const names = Object.keys(src.bindings);
|
|
26303
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
26304
|
+
for (const name of names) {
|
|
26305
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
26306
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
26307
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
26316
26308
|
}
|
|
26317
|
-
|
|
26318
|
-
|
|
26319
|
-
|
|
26320
|
-
|
|
26321
|
-
|
|
26322
|
-
|
|
26323
|
-
* is discarded — the only caller (`mergeLinkedStatus`) re-validates the result
|
|
26324
|
-
* with the target cap's Zod schema and rejects a malformed overlay.
|
|
26325
|
-
* The return type `T` preserves the OUTER shape; the rewritten key is not
|
|
26326
|
-
* narrowed, so callers should re-validate against the status schema. */
|
|
26327
|
-
function setByPath(obj, path, value) {
|
|
26328
|
-
const dotIndex = path.indexOf(".");
|
|
26329
|
-
if (dotIndex === -1) return {
|
|
26330
|
-
...obj,
|
|
26331
|
-
[path]: value
|
|
26332
|
-
};
|
|
26333
|
-
const head = path.slice(0, dotIndex);
|
|
26334
|
-
const tail = path.slice(dotIndex + 1);
|
|
26335
|
-
const child = obj[head];
|
|
26336
|
-
const childObj = isRecord(child) ? child : {};
|
|
26337
|
-
return {
|
|
26338
|
-
...obj,
|
|
26339
|
-
[head]: setByPath(childObj, tail, value)
|
|
26340
|
-
};
|
|
26341
|
-
}
|
|
26342
|
-
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
|
|
26343
|
-
* Zod v4 exposes `.unwrap()` on all three wrapper classes. */
|
|
26344
|
-
function unwrap(schema) {
|
|
26345
|
-
let s = schema;
|
|
26346
|
-
while (s instanceof z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
|
|
26347
|
-
return s;
|
|
26348
|
-
}
|
|
26349
|
-
function leafKind(schema) {
|
|
26350
|
-
const s = unwrap(schema);
|
|
26351
|
-
if (s instanceof z.ZodEnum) return {
|
|
26352
|
-
kind: "enum",
|
|
26353
|
-
enumValues: s.options
|
|
26354
|
-
};
|
|
26355
|
-
if (s instanceof z.ZodNumber) return { kind: "number" };
|
|
26356
|
-
if (s instanceof z.ZodString) return { kind: "string" };
|
|
26357
|
-
if (s instanceof z.ZodBoolean) return { kind: "boolean" };
|
|
26358
|
-
return null;
|
|
26359
|
-
}
|
|
26360
|
-
/** Walk a cap status schema into flat wireable leaf fields (dotted paths).
|
|
26361
|
-
* Recurses into nested ZodObject (unwrapping nullable/optional/default first),
|
|
26362
|
-
* so fields with null live values are still offered. Arrays / unknown shapes
|
|
26363
|
-
* are skipped — never throws. */
|
|
26364
|
-
function enumerateSchemaFields(schema, prefix = "") {
|
|
26365
|
-
const root = unwrap(schema);
|
|
26366
|
-
if (!(root instanceof z.ZodObject)) return [];
|
|
26367
|
-
const out = [];
|
|
26368
|
-
const shape = root.shape;
|
|
26369
|
-
for (const [key, field] of Object.entries(shape)) {
|
|
26370
|
-
const path = prefix ? `${prefix}.${key}` : key;
|
|
26371
|
-
const inner = unwrap(field);
|
|
26372
|
-
if (inner instanceof z.ZodObject) {
|
|
26373
|
-
out.push(...enumerateSchemaFields(inner, path));
|
|
26374
|
-
continue;
|
|
26375
|
-
}
|
|
26376
|
-
const leaf = leafKind(field);
|
|
26377
|
-
if (leaf) out.push({
|
|
26378
|
-
path,
|
|
26379
|
-
kind: leaf.kind,
|
|
26380
|
-
...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
|
|
26381
|
-
});
|
|
26309
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
26310
|
+
if (!compiled.ok) return compiled.error;
|
|
26311
|
+
const bound = new Set(names);
|
|
26312
|
+
for (const id of compiled.parsed.identifiers) {
|
|
26313
|
+
if (id === "now") continue;
|
|
26314
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
26382
26315
|
}
|
|
26383
|
-
return
|
|
26384
|
-
}
|
|
26385
|
-
/** Enumerate the per-item wireable fields of an item-array cap (see
|
|
26386
|
-
* `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
|
|
26387
|
-
* `keyField` (the key comes from the link's `itemKey`, never from a wired
|
|
26388
|
-
* source), each tagged `item: true` so the authoring UI collects an
|
|
26389
|
-
* `itemKey` alongside the field. Never throws. */
|
|
26390
|
-
function enumerateItemArrayFields(itemArray) {
|
|
26391
|
-
return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
|
|
26392
|
-
...f,
|
|
26393
|
-
item: true
|
|
26394
|
-
}));
|
|
26316
|
+
return null;
|
|
26395
26317
|
}
|
|
26396
26318
|
/**
|
|
26319
|
+
* What an expression's named bindings READ from.
|
|
26320
|
+
*
|
|
26321
|
+
* Salvaged verbatim from the deleted device-link mechanism. Wiring's source
|
|
26322
|
+
* kinds were the one part of it worth keeping: addressing a device field by
|
|
26323
|
+
* re-sync-stable `stableId`, a per-device constant, and a sibling-accessory
|
|
26324
|
+
* read are the vocabulary any cross-device derivation needs, and they were
|
|
26325
|
+
* already correct. What wiring got wrong was the DESTINATION — a field on
|
|
26326
|
+
* somebody else's device, with no identity — not the source.
|
|
26327
|
+
*
|
|
26328
|
+
* These shapes are therefore kept, re-homed next to the engine that consumes
|
|
26329
|
+
* them, and are the binding type of a composition recipe (the source picker
|
|
26330
|
+
* stays `deviceManager.getWireableFields`). They deliberately do NOT nest: a
|
|
26331
|
+
* binding is a read, never another expression.
|
|
26332
|
+
*
|
|
26333
|
+
* Schemas are authoritative; every type is `z.infer` of one, so a wire shape and
|
|
26334
|
+
* a TypeScript shape cannot drift apart (`scripts/check-schema-type-twins.ts`).
|
|
26335
|
+
*/
|
|
26336
|
+
/** Read a sibling accessory's status field, addressed by the sibling's key.
|
|
26337
|
+
* `kind` is optional for wire compatibility — absent means `'field'`. */
|
|
26338
|
+
var ExpressionFieldBindingSchema = z.object({
|
|
26339
|
+
kind: z.literal("field").optional(),
|
|
26340
|
+
sourceKey: z.string(),
|
|
26341
|
+
cap: z.string(),
|
|
26342
|
+
fieldPath: z.string()
|
|
26343
|
+
});
|
|
26344
|
+
/** A constant. No device is read. */
|
|
26345
|
+
var ExpressionLiteralBindingSchema = z.object({
|
|
26346
|
+
kind: z.literal("literal"),
|
|
26347
|
+
value: z.union([
|
|
26348
|
+
z.string(),
|
|
26349
|
+
z.number(),
|
|
26350
|
+
z.boolean(),
|
|
26351
|
+
z.null()
|
|
26352
|
+
])
|
|
26353
|
+
});
|
|
26354
|
+
/** Read ANY device's status field, addressed by its re-sync-stable `stableId` —
|
|
26355
|
+
* never by numeric id, which a re-adoption reissues. */
|
|
26356
|
+
var ExpressionGlobalBindingSchema = z.object({
|
|
26357
|
+
kind: z.literal("global"),
|
|
26358
|
+
sourceStableId: z.string(),
|
|
26359
|
+
cap: z.string(),
|
|
26360
|
+
fieldPath: z.string()
|
|
26361
|
+
});
|
|
26362
|
+
var ExpressionBindingSourceSchema = z.union([
|
|
26363
|
+
ExpressionFieldBindingSchema,
|
|
26364
|
+
ExpressionLiteralBindingSchema,
|
|
26365
|
+
ExpressionGlobalBindingSchema
|
|
26366
|
+
]);
|
|
26367
|
+
z.object({
|
|
26368
|
+
expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
26369
|
+
bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
26370
|
+
}).superRefine((src, ctx) => {
|
|
26371
|
+
const err = validateExpressionSource(src);
|
|
26372
|
+
if (err !== null) ctx.addIssue({
|
|
26373
|
+
code: "custom",
|
|
26374
|
+
message: err,
|
|
26375
|
+
path: ["expr"]
|
|
26376
|
+
});
|
|
26377
|
+
});
|
|
26378
|
+
/**
|
|
26397
26379
|
* Runtime list of cap names with status. Used by the settings
|
|
26398
26380
|
* aggregator to enumerate caps whose `status` should be polled +
|
|
26399
26381
|
* streamed via `subscribeDeviceStatusAggregate`.
|
|
@@ -27449,6 +27431,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
27449
27431
|
addonId: null,
|
|
27450
27432
|
access: "view"
|
|
27451
27433
|
},
|
|
27434
|
+
"coreBlocks.restart": {
|
|
27435
|
+
capName: "core-blocks",
|
|
27436
|
+
capScope: "system",
|
|
27437
|
+
addonId: null,
|
|
27438
|
+
access: "create"
|
|
27439
|
+
},
|
|
27452
27440
|
"coreBlocks.setEnabled": {
|
|
27453
27441
|
capName: "core-blocks",
|
|
27454
27442
|
capScope: "system",
|
|
@@ -28085,12 +28073,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
28085
28073
|
addonId: null,
|
|
28086
28074
|
access: "create"
|
|
28087
28075
|
},
|
|
28088
|
-
"deviceManager.setDeviceLinks": {
|
|
28089
|
-
capName: "device-manager",
|
|
28090
|
-
capScope: "system",
|
|
28091
|
-
addonId: null,
|
|
28092
|
-
access: "create"
|
|
28093
|
-
},
|
|
28094
28076
|
"deviceManager.setDisabled": {
|
|
28095
28077
|
capName: "device-manager",
|
|
28096
28078
|
capScope: "system",
|
|
@@ -31235,6 +31217,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
31235
31217
|
addonId: null,
|
|
31236
31218
|
access: "create"
|
|
31237
31219
|
},
|
|
31220
|
+
"streamBroker.fetchEventMedia": {
|
|
31221
|
+
capName: "stream-broker",
|
|
31222
|
+
capScope: "system",
|
|
31223
|
+
addonId: null,
|
|
31224
|
+
access: "create"
|
|
31225
|
+
},
|
|
31238
31226
|
"streamBroker.getAllRtspEntries": {
|
|
31239
31227
|
capName: "stream-broker",
|
|
31240
31228
|
capScope: "system",
|
|
@@ -31319,6 +31307,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
31319
31307
|
addonId: null,
|
|
31320
31308
|
access: "create"
|
|
31321
31309
|
},
|
|
31310
|
+
"streamBroker.produceEventMedia": {
|
|
31311
|
+
capName: "stream-broker",
|
|
31312
|
+
capScope: "system",
|
|
31313
|
+
addonId: null,
|
|
31314
|
+
access: "create"
|
|
31315
|
+
},
|
|
31322
31316
|
"streamBroker.publishCameraStream": {
|
|
31323
31317
|
capName: "stream-broker",
|
|
31324
31318
|
capScope: "system",
|
|
@@ -32115,60 +32109,6 @@ TimelapseRuleInputSchema.extend({
|
|
|
32115
32109
|
createdAt: z.number(),
|
|
32116
32110
|
updatedAt: z.number()
|
|
32117
32111
|
});
|
|
32118
|
-
/**
|
|
32119
|
-
* The addon id as the REGISTRY, the wire and the durable stores spell it.
|
|
32120
|
-
*
|
|
32121
|
-
* `AddonContext.id` does not agree with itself across the two context
|
|
32122
|
-
* factories:
|
|
32123
|
-
*
|
|
32124
|
-
* - a FORKED addon gets the bare manifest id
|
|
32125
|
-
* (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
|
|
32126
|
-
* - an addon co-located in hub-main gets it PREFIXED
|
|
32127
|
-
* (`server/backend/src/core/addon/addon-registry.service.ts` →
|
|
32128
|
-
* ``id: `addon:${addonId}` ``).
|
|
32129
|
-
*
|
|
32130
|
-
* Everything an addon might compare `ctx.id` AGAINST carries the bare form:
|
|
32131
|
-
* `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
|
|
32132
|
-
* `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
|
|
32133
|
-
*
|
|
32134
|
-
* So `entry.providerAddonId === this.ctx.id` is silently, permanently false
|
|
32135
|
-
* for a builtin — and only for a builtin, which is why it survives a green
|
|
32136
|
-
* suite whose fake supplies the bare id. That is exactly how camera 615's
|
|
32137
|
-
* virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
|
|
32138
|
-
* binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
|
|
32139
|
-
*
|
|
32140
|
-
* Route every comparison between `ctx.id` and a registry/store addon id
|
|
32141
|
-
* through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
|
|
32142
|
-
* enforces it in the processes where the prefix exists.
|
|
32143
|
-
*/
|
|
32144
|
-
/** The prefix the hub-main context factory prepends to the manifest id. */
|
|
32145
|
-
var ADDON_ID_PREFIX = "addon:";
|
|
32146
|
-
/** The manifest id, whichever spelling of `ctx.id` you were handed. */
|
|
32147
|
-
function bareAddonId(id) {
|
|
32148
|
-
return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
|
|
32149
|
-
}
|
|
32150
|
-
/** True when both ids name the same addon, prefixed or not. */
|
|
32151
|
-
function isSameAddonId(a, b) {
|
|
32152
|
-
return bareAddonId(a) === bareAddonId(b);
|
|
32153
|
-
}
|
|
32154
|
-
/**
|
|
32155
|
-
* Decode base64 little-endian Float32 back into a vector.
|
|
32156
|
-
*
|
|
32157
|
-
* Throws on a byte length that is not a multiple of 4 — a truncated vector
|
|
32158
|
-
* would otherwise silently rank against a shorter one and produce a plausible
|
|
32159
|
-
* score, which is worse than an error.
|
|
32160
|
-
*/
|
|
32161
|
-
function decodeVectorBase64(encoded) {
|
|
32162
|
-
const buffer = Buffer.from(encoded, "base64");
|
|
32163
|
-
if (buffer.byteLength % 4 !== 0) throw new Error(`decodeVectorBase64: ${buffer.byteLength} bytes is not a whole number of Float32 values`);
|
|
32164
|
-
const out = new Float32Array(buffer.byteLength / 4);
|
|
32165
|
-
for (let i = 0; i < out.length; i += 1) out[i] = buffer.readFloatLE(i * 4);
|
|
32166
|
-
return out;
|
|
32167
|
-
}
|
|
32168
|
-
/** Vector length implied by a base64 payload, without decoding it. */
|
|
32169
|
-
function vectorDimFromBase64(encoded) {
|
|
32170
|
-
return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
|
|
32171
|
-
}
|
|
32172
32112
|
z.object({
|
|
32173
32113
|
/**
|
|
32174
32114
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -32276,6 +32216,60 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
|
32276
32216
|
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
32277
32217
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
32278
32218
|
/**
|
|
32219
|
+
* The addon id as the REGISTRY, the wire and the durable stores spell it.
|
|
32220
|
+
*
|
|
32221
|
+
* `AddonContext.id` does not agree with itself across the two context
|
|
32222
|
+
* factories:
|
|
32223
|
+
*
|
|
32224
|
+
* - a FORKED addon gets the bare manifest id
|
|
32225
|
+
* (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
|
|
32226
|
+
* - an addon co-located in hub-main gets it PREFIXED
|
|
32227
|
+
* (`server/backend/src/core/addon/addon-registry.service.ts` →
|
|
32228
|
+
* ``id: `addon:${addonId}` ``).
|
|
32229
|
+
*
|
|
32230
|
+
* Everything an addon might compare `ctx.id` AGAINST carries the bare form:
|
|
32231
|
+
* `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
|
|
32232
|
+
* `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
|
|
32233
|
+
*
|
|
32234
|
+
* So `entry.providerAddonId === this.ctx.id` is silently, permanently false
|
|
32235
|
+
* for a builtin — and only for a builtin, which is why it survives a green
|
|
32236
|
+
* suite whose fake supplies the bare id. That is exactly how camera 615's
|
|
32237
|
+
* virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
|
|
32238
|
+
* binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
|
|
32239
|
+
*
|
|
32240
|
+
* Route every comparison between `ctx.id` and a registry/store addon id
|
|
32241
|
+
* through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
|
|
32242
|
+
* enforces it in the processes where the prefix exists.
|
|
32243
|
+
*/
|
|
32244
|
+
/** The prefix the hub-main context factory prepends to the manifest id. */
|
|
32245
|
+
var ADDON_ID_PREFIX = "addon:";
|
|
32246
|
+
/** The manifest id, whichever spelling of `ctx.id` you were handed. */
|
|
32247
|
+
function bareAddonId(id) {
|
|
32248
|
+
return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
|
|
32249
|
+
}
|
|
32250
|
+
/** True when both ids name the same addon, prefixed or not. */
|
|
32251
|
+
function isSameAddonId(a, b) {
|
|
32252
|
+
return bareAddonId(a) === bareAddonId(b);
|
|
32253
|
+
}
|
|
32254
|
+
/**
|
|
32255
|
+
* Decode base64 little-endian Float32 back into a vector.
|
|
32256
|
+
*
|
|
32257
|
+
* Throws on a byte length that is not a multiple of 4 — a truncated vector
|
|
32258
|
+
* would otherwise silently rank against a shorter one and produce a plausible
|
|
32259
|
+
* score, which is worse than an error.
|
|
32260
|
+
*/
|
|
32261
|
+
function decodeVectorBase64(encoded) {
|
|
32262
|
+
const buffer = Buffer.from(encoded, "base64");
|
|
32263
|
+
if (buffer.byteLength % 4 !== 0) throw new Error(`decodeVectorBase64: ${buffer.byteLength} bytes is not a whole number of Float32 values`);
|
|
32264
|
+
const out = new Float32Array(buffer.byteLength / 4);
|
|
32265
|
+
for (let i = 0; i < out.length; i += 1) out[i] = buffer.readFloatLE(i * 4);
|
|
32266
|
+
return out;
|
|
32267
|
+
}
|
|
32268
|
+
/** Vector length implied by a base64 payload, without decoding it. */
|
|
32269
|
+
function vectorDimFromBase64(encoded) {
|
|
32270
|
+
return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
|
|
32271
|
+
}
|
|
32272
|
+
/**
|
|
32279
32273
|
* Scores all applicable inference backends for the given hardware.
|
|
32280
32274
|
*
|
|
32281
32275
|
* Rules (hardware-driven only — no Python import probes):
|
|
@@ -32389,4 +32383,4 @@ function enumerateInferenceDevices(hw) {
|
|
|
32389
32383
|
return out;
|
|
32390
32384
|
}
|
|
32391
32385
|
//#endregion
|
|
32392
|
-
export {
|
|
32386
|
+
export { platformProbeCapability as $, deviceStatusCapability as A, nodePin as At, isSameAddonId as B, coreBlockAddonId as C, asString as Ct, decodeVectorBase64 as D, expandCapMethods as Dt, dataStoreProviderCapability as E, emitReadiness as Et, extractNestedAddonId as F, scopeKey as Ft, logDestinationCapability as G, kebabToCamel as H, filesystemBrowseCapability as I, sleep as It, metricsProviderCapability as J, logLevelAtMost as K, isArrayOutputSchema as L, EventCategory as Lt, enumerateInferenceDevices as M, parseJsonUnknown as Mt, enumerateItemArrayFields as N, readinessKey as Nt, deviceManagerCapability as O, hydrateSchema as Ot, enumerateSchemaFields as P, resolveCapMount as Pt, parseStreamParamsFormPatch as Q, isCollectionArrayMethod as R, buildStreamParamsConfigSchema as S, asNumber as St, coreBlocksCapability as T, emitDownForOwnedCaps as Tt, lifecycleJobSchema as U, isVoidInput as V, localNetworkCapability as W, oauthIntegrationCapability as X, normalizeUnit as Y, objectInputDeclaresAddonId as Z, addonWidgetsCapability as _, DeviceType as _t, CAP_NAMES_WITH_STATUS as a, storageProviderCapability as at, backupCapability as b, WELL_KNOWN_TAB_MAP as bt, DeviceStatusSchema as c, vectorDimFromBase64 as ct, STREAM_PROFILE_META as d, BaseAddon as dt, procedureAuthKey as et, ScopedTokenSchema as f, DATAPLANE_SECRET_HEADER as ft, addonSettingsCapability as g, DeviceRole as gt, addonPagesCapability as h, DeviceFeature as ht, BatteryStatusSchema as i, storageCapability as it, doorbellCapability as j, parseJsonObject as jt, deviceStateCapability as k, isDeviceConfigCap as kt, METHOD_ACCESS_MAP as l, vectorStoreCapability as lt, UserRecordSchema as m, DEVICE_STATUS_METHOD as mt, AlertSchema as n, settingsStoreCapability as nt, CORE_BLOCK_ADDON_PREFIX as o, streamQualityLabel as ot, StorageLocationTypeSchema as p, DEVICE_SETTINGS_CONTRIBUTION_METHODS as pt, looseSchema as q, ApiKeyRecordSchema as r, snapshotCapability as rt, CoreBlockSchema as s, userManagementCapability as st, ALL_CAPABILITY_DEFINITIONS as t, scoreRuntimes as tt, RUNTIME_DEFAULTS as u, errMsg as ut, alertsCapability as v, ReadinessRegistry as vt, coreBlockIdFromAddonId as w, createEvent as wt, bareAddonId as x, asJsonObject as xt, authProviderCapability as y, ReadinessTimeoutError as yt, isObjectInput as z };
|